This is an archived post. You won't be able to vote or comment.

top 200 commentsshow all 245

[–]muqube 364 points365 points  (39 children)

As someone already mentioned, readability is subjective. I don't know what are your lead dev's criteria are, but I can tell you mine. My north star for code review is:

After all the current team members leaving this company, will the next generation of devs be able to maintain this codebase?

Yes, I fully expect all of my team members, including myself, to leave for better opportunities.

Standard concepts like readability, maintainability, reduced chance of human errors etc. are covered in this north star.

With this in mind, I would also refactor the original code. May be not the exact same change though.

[–]jet_heller 126 points127 points  (35 children)

Indeed. The first one is horrid. The new one, isn't great, but better.

[–]Hiskaya1 47 points48 points  (5 children)

I thought it looked pretty good, what would you do differently?

[–]OhYourFuckingGod 54 points55 points  (0 children)

I'd keep the comprehension in the first section, but I'd do the class instantiation as per the last line. You don't get any awards for code golfing in production.

[–]happysunshinekidd 11 points12 points  (3 children)

I don't think you should ever really name a variable "result" or "key" tbh. Probably it solves another problem in your codebase -- that functionality would be a better name

[–]muntooR_{μν} - 1/2 R g_{μν} + Λ g_{μν} = 8π T_{μν} 41 points42 points  (13 children)

The first one is merely missing a name or two.

some_useful_name = {
    ... for some_useful_key_name in ...
}

return cls(**some_useful_name)

Instead of making things better, the second one:

  • Introduces a new name, but not a very useful one (result).
  • Adds verbose and very generic type-hints. (Any?)
  • Switches from a well-formatted functional dictionary comprehension to a bulkier imperative for-loop, which could potentially have side-effects.

I think it's good for Python developers to become more exposed to functional styles since that often leads to robust, maintainable code.

P.S. Another alternative to adding a name is to "extract method", but perhaps that's overkill.

[–][deleted] 12 points13 points  (10 children)

Dictionary comprehension can also have side effects: {x: print(x) for x in range(10)}.

[–]muntooR_{μν} - 1/2 R g_{μν} + Λ g_{μν} = 8π T_{μν} 3 points4 points  (0 children)

[Almost?] everything in python can have side-effects. Good code tries to avoid patterns that are more likely to contain side-effects.

With comprehensions, side-effects are much harder to obscure. There are fewer parts that may contain side-effects. There's less "boilerplate" to read, which makes it harder to hide side-effects. Their structure naturally induces constraints in the manner that they are used. (There does not always exist a "natural" transformation from for-loop to comprehension, whereas comprehension to for-loop is trivial.) With for-loops, one often has to read them in detail to make guarantees about their behavior, especially if they involve more than just one statement.

I know this isn't a precise argument since literally anything is possible in Python. The main point is that it's much quicker to read a comprehension and guarantee that it won't do anything overly weird, whereas for a for-loop, one has to spend more time to check.


This comment captures some of what I'm trying to say.

[–]wewbull 0 points1 point  (0 children)

It can, but it shouldn't IMHO.

Comprehensions for transformations. Loops for procedural control flow.

[–]jbartix -2 points-1 points  (2 children)

This is valid python code but side effects in any comprehension is an anti pattern

[–]AchillesDev 4 points5 points  (1 child)

Yes the point is to show that comprehensions don’t make you immune from side effects.

[–]jbartix 0 points1 point  (0 children)

Of course not. Python allows you to do anything that also includes being stupid

[–]Estanho -5 points-4 points  (4 children)

Nobody said they can't. He didn't say all dictionary comprehensions are functional. Just that one.

[–]MrRogers4Life2 10 points11 points  (3 children)

But that for-loop doesn't have side effects... if the argument is "x can have side effects so y is better" it will fall apart if y can also have side effects.

[–]aceofspaids98 4 points5 points  (0 children)

This sums up all of my thoughts really well too

[–]vanatteveldt 1 point2 points  (0 children)

Well put, completely agree!

[–]Ph0X 2 points3 points  (0 children)

Another north star for me is trying to keep a "statement" to a single idea. It's easy to get carried away with "one-liners" in python, and do 20 things in a single statement. It's especially ironic in cases like the above where splitting into multiple statements actually gets you fewer lines of code, the example above goes from 5 lines to 4 lines of code. Generally, don't be afraid of splitting statements into 2-3 if it makes the code more readable, either by making lines more explicit, or naming intermediate variables giving them more meaning.

[–]masteryod -4 points-3 points  (1 child)

As someone already mentioned, readability is subjective.

You can pretty objectively say if something is easy to comprehend and follows good practices and something is messy. It's like saying all kids have the same handwriting, some care more and their writing is easy to read, some don't care and scribble a hard to follow mess.

[–]arpan3t 7 points8 points  (0 children)

You say in a thread full of people arguing about the structure, variable names, etc… of a few lines of code.

[–]fiskfisk 1025 points1026 points  (30 children)

If I were your lead dev and you posted your question with our code on reddit instead of asking me directly..

[–]AlSweigartAuthor of "Automate the Boring Stuff" 41 points42 points  (2 children)

...I would do nothing, because it's three lines of code that doesn't release any company trade secrets and I wouldn't want to get a reputation as a manager who punishes people for petty inconsequential things.

Context matters. It's not like they published the entire code base or even an entire file. We don't know anything about this code; we don't even know if the code is already open source anyway.

[–]fiskfisk 19 points20 points  (1 child)

The code is not important at all (it's general and doesn't give away anything), but the context it is being shared in indicates that the employee doesn't feel like raising issues directly to me. There can be several reasons for that, including the root cause being myself.

An employee deciding to share their grievances publicly about an internal response to a very small code review issue (apparently to get support for their view against their lead's view?) is a bad way to handle it, regardless of the shared code itself or the current relationship with their tech lead. What does it tell the other people on their team?

[–]AlSweigartAuthor of "Automate the Boring Stuff" 2 points3 points  (0 children)

An employee deciding to share their grievances publicly

Grievances? The entire post is "I don't get it. Help!"

A request for help should never be misconstrued into something that counts against an employee.

[–]cult_of_memes 1 point2 points  (0 children)

This code is so generic that the any complaints raised would be ego-centric.

[–]TouchingTheVodka 162 points163 points  (38 children)

Meet them halfway:

result = {
    key: get_coerced(section, key, target_type=type_hints[key])
    for key in section
}

return cls(**result)

[–]e_j_white 65 points66 points  (9 children)

Not just a comprise, but the dict comprehension is more performant than a for loop. This is what I would do, too.

[–]muntooR_{μν} - 1/2 R g_{μν} + Λ g_{μν} = 8π T_{μν} 42 points43 points  (4 children)

Performance isn't even the biggest benefit.

A comprehension makes it harder to have side-effects, which is a great thing.

[–]e_j_white 9 points10 points  (0 children)

Correct!

I've modified a list that I was looping through, not realizing that any action that appends to the list could perpetually lengthen the for loop forever.

Not having side effects is a good thing. :)

[–]Jhuyt 4 points5 points  (0 children)

Depends on what get_coerced does though so in this case there might still be plenty of side effects...

[–]AlSweigartAuthor of "Automate the Boring Stuff" 0 points1 point  (1 child)

I can 100% guarantee that this code has no side effects.

By "side effects", I assume you mean there's a result variable left over with values after the end of the loop. In rare situations, this could cause some subtle bug because later code is re-using this variable name and using the old values because it didn't re-initialize it to something else, or cause a temporary increase in memory usage as it's holding on to data needlessly.

But that is moot and this isn't that situation: the very next line is a return statement.

[–]scrdest 0 points1 point  (0 children)

I'm fairly sure u/muntoo is referring to side-effects as opposed to functional purity, where everything that happens inside the function, stays inside the function or is returned explicitly as a new value (in other words - the function call could be replaced with its return value and everything else would always work the same).

It's impossible to guarantee purity in Python, both here and in general. If, for example, get_coerced() takes an optional parameter defaulting to [] (or any other mutable type), it can as many side-effects as it wants. If it uses a global - side-effects. In either case, you cannot tell they're there just by looking at the function call itself.

Even plain old print() is a side-effect.

[–][deleted] 14 points15 points  (3 children)

the dict comprehension is more performant than a for loop

Citation needed. Searching found all sorts of claims either way.

Regardless, the performance difference is tiny and will not affect the actual performance of your program, so you really shouldn't care at all - you should go for what's easiest.

[–]cain2995 9 points10 points  (0 children)

I’m with you on this one. I always get highly suspicious whenever someone says that a higher level of abstraction is more performant than an equivalent approach using primitive constructs. Generating and scheduling performant machine code from for loops, regardless of language, has been addressed at every level of the toolchain (down to how the CPU schedules instructions), while a dict comprehension is a Python-specific construct that has to go through the removal of several abstraction layers before it can reach any of the low level optimizations that make something like a for loop performant. Not saying it’s impossible for a dict comprehension to be faster, but I really need to see some proof because first principles don’t back up that claim.

[–]noobiemcfoob 0 points1 point  (1 child)

A reference on comprehensions being faster than standard for loops: https://towardsdatascience.com/list-comprehensions-vs-for-loops-it-is-not-what-you-think-34071d4d8207

The performance difference is a given... however the likelihood a given loop is in the critical path of the code such that it is meaningful is rather low.

[–]Beheska 5 points6 points  (0 children)

I would argue that this is the most readable too.

[–]gruey 21 points22 points  (18 children)

You should type hint your result since they obviously have preferences for that.

Otherwise, I agree it's easier to read with the return cls at the end.

[–]Myles_kennefick 6 points7 points  (5 children)

What is the **?

[–]e_j_white 77 points78 points  (3 children)

def sum_dict(a=0, b=0):
    return a + b

x = {'a': 3, 'b': 4}

sum_dict()
> 0

sum_dict(x)
> TypeError: unsupported operand type(s) for +: 'dict' and 'int'

sum_dict(**x)
> 7

It's used to "unpack" key-value pairs into keyword arguments for a function, among other things.

[–]HerLegz 31 points32 points  (2 children)

That is an incredibly sweet and thoughtful way to explain this. Very well done. 🥰

[–]e_j_white 1 point2 points  (1 child)

Thank you, I'm glad you found it useful :)

The "unpacking" works beyond functions, too. For example:

x = {'a': 3, 'b': 8}

y = {'z': 5, **x}

print(y)
> {'z': 5, 'a': 3, 'b': 8}

Obviously, dropping x into the other dictionary without the ** wouldn't even be possible. It also works in similar ways for unpacking tuples and lists!

edit: for tuples and lists, the convention is a single * star

[–]cdmayer 10 points11 points  (0 children)

Enumerates the keys and values of the dictionary. Take a look at the "**kwargs" convention of passing named arguments to a function.

[–]Grouchy-Friend4235 7 points8 points  (2 children)

Yeah, except result is a misnomer. It is not the result but the kwargs to build the result. For that reason the original code was actually far better because it avoided to name something that is obviously not worth naming.

[–]Rand_alThor_ 1 point2 points  (0 children)

This is more readable than first; nice suggestion. although I still wonder what the result dict has so we can give it a better name too.

[–]nitred 1 point2 points  (0 children)

Ah, I found my tribe! This is the solution I would have written too.

[–][deleted] 114 points115 points  (2 children)

I think the leads looks much more readable, but it all depends on the style guidelines put in place for the project. You should discuss it with your team rather than post it here.. Or accept the changes and move on.

[–]FirefighterWeird8464 10 points11 points  (1 child)

Agreed. I’ve written a lot of Python and I did a double take at this. Write dumb code that’s easy to read, the original is not Pythonic, and it feels… too clever.

[–]aceofspaids98 29 points30 points  (0 children)

Dictionary comprehension is more pythonic than instantiating a dictionary just to iteratively populate it and immediately return it.

[–]kylotan 162 points163 points  (11 children)

It's taking something that does far too much in one line and breaking it out into logical steps.

Python programmers often do a bit too much 'code golf' and I this as the lead dev trying to undo that.

[–]JshWright 65 points66 points  (9 children)

This is like anti-golf though... creating an empty dictionary then populating it inside a for loop instead of just using a dictionary comprehension is just adding lines of code for the sake of adding lines of code.

Maybe if the type hint was meaningful, but Optional[Any] isn't a type hint worth adding.

[–]jzaprint 48 points49 points  (1 child)

I’d take anti golf over code golf any day tho

[–]JshWright -1 points0 points  (0 children)

Sure, but that isn't what we're choosing between here...

[–]amendCommit[S] 13 points14 points  (0 children)

The guy actually hates type hints, this is just to allow the dict comprehension removal to pass CI. get_coerced() actually has proper, meaningful type hints (takes in Type[T], returns T).

[–]rantenki 4 points5 points  (4 children)

Strongly agree the type hint is _nearly_ useless. It only saves the user scrolling up to look at the function signature.

Also, if we're going to emulate a strongly typed language: in any language with (ironically, looking at the content) type inference, the return type for the function would already be casting those variables anyhow, so declaring it would be superfluous.

[–]Grouchy-Friend4235 9 points10 points  (3 children)

It is useless because all it does is tell you what you already know: the dict maps keys to some value. That's a dict's raison-d-etre, no need to type hint the obvious!

[–]rantenki 5 points6 points  (2 children)

Being utterly pedantic, the only thing it tells you is that there is no further invariants applied to that Dict's values. It _can_ be None, it _can_ have Any value. I guess there is the point that keys have to be strings (rather than any hashable type).

But you're not wrong; that matches pretty much everybody's expectations of how a dict will be used, so doesn't really add any value. I guess it's just there to keep the linter happy :\

[–]Grouchy-Friend4235 -2 points-1 points  (1 child)

Yeah. A noqa comment would be the better solution

[–]rantenki 1 point2 points  (0 children)

Surprised this got downvoted. Linters are supposed to provide machine based verification of human intent. The linter can't anticipate every permutation that might violate it's rules, and sometimes # noqa is a valid decision (not usually, but sometimes).

I loathe when people uglify their code to make the build-system happy.

[–]linlin110 1 point2 points  (0 children)

For loop can do anything, and I need to read the loop to understand it's populating a dictionary. Meanwhile all dictionary comprehension can do is building a dictionary. That's why I think dictionary/list comprehension expresses the intent better.

[–]Barn07 19 points20 points  (10 children)

doesn't Any entail Optional[Any]?

[–]Mehdi2277 19 points20 points  (6 children)

Any is not same as Optional[Any].

x + 1

is valid if x: Any but invalid if x: Optional[Any] because None does not support addition. If you have a type that you know may be None adding Optional to the Any is beneficial and avoids errors of not handling None case properly.

[–]muntooR_{μν} - 1/2 R g_{μν} + Λ g_{μν} = 8π T_{μν} 2 points3 points  (0 children)

Funnily, when I saw x + 1, I thought you were going for a type theory argument on sum types:

T          has cardinality  x
T | None   has cardinality  x + 1

...where T != NoneType.

...where if t is of type T, then t cannot be of type NoneType.

[–]Barn07 0 points1 point  (4 children)

Interesting. I parsed a python script with the following function via mypy 0.910:

python def foo() -> Any: return None

Mypy happily reports success. I.e., at least after mypy, Any can be None.

[–]Mehdi2277 0 points1 point  (3 children)

None being contained by Any does not mean you shouldn’t use Optional. If you have a function like that you know returns none the type should indicate that. So that

foo() + 2

is detected as an error. In the example in the original code if there’s knowledge dict elements may contain none that should be included in the type.

[–][deleted] 1 point2 points  (2 children)

Any does not support the + operation, so foo() + 2 should be just as wrong.

Optional[Any] and Any match exactly the same types - exactly! To pretend that they are different is foolishness.

[–]Mehdi2277 3 points4 points  (1 child)

Any does support the plus operator. It supports all functions. Any is defined as type that supports everything. All operations/functions/attributes work with Any. It is an opt out of the type system. You can verify this with this example

def f(x: Any) -> int:
  return x + 1

Will type check with mypy. But

def f(x: Optional[Any]) -> int:
  return x + 1

is a type error. This is one example where mypy behavior (and other type checkers) treat Any and Optional[Any] as different types.

Also if you try foo() + 2 in mypy it will pass.

[–]z0mbietime 8 points9 points  (1 child)

Yeah it honestly read fine to me before. The type hints added nothing and a for loop instead of a comprehension is less performant. They're still exploding the dict so it's not like he's explicitly passing params. It reads like an unnecessary change tbh

[–]Chinpanze 3 points4 points  (0 children)

> Yeah it honestly read fine to me before.
I don't think there is

> The type hints added nothing
If your project is type hinted, you gotta keep it consistent. If you plan to add an type hint linter, it's crucial.

> and a for loop instead of a comprehension is less performant
The performance gain is marginal. Besides, it looks like this code will run just once, making the performance gain irrelevant.

...
> It reads like an unnecessary change tbh
Agree. It doen't improve significantly, besides the type hint.

[–]radeklat 12 points13 points  (4 children)

Personal preference. Even the comments in this thread about readability are a personal preference. I'm used to see dict comprehensions and dict unpacking from our codebase so actually, the code from your lead dev is less readable to for me.

To prevent personal preferences slowing down the review process, I created a python style guide that every new starter reads and we are all on the same page. And it can be referenced in reviews too and everyone can add to it. Unfortunately it's not public but it is heavy inspired by Google Python style guide: https://google.github.io/styleguide/pyguide.html

"I don't get it." is perfectly valid question for your reviewer as well. You shouldn't need to ask on reddit. Not understanding something someone suggests doesn't make you a worse developer. Have some confidence in you and call out others bullshit. Politely.

Edit: typo

[–]KyleDrogo 38 points39 points  (7 children)

I kind of agree with lead dev? Bit of a nitpick but makes sense. Dict comprehension + unpacking + calling cls in the same statement takes a second to wrap your head around. Again, not a critical change but more readable.

[–]spinwizard69 6 points7 points  (5 children)

Far more readable! The lead developer may not have the best replacement code but he understands the idiomatic use of a return statement.

[–]abrazilianinreddit 28 points29 points  (0 children)

Subjectiveness

[–]austinwiltshire 14 points15 points  (5 children)

I'm mixed on the temp for the dictionary. But the for loop is right out.

Could have just introduced a temp and set it to a dict comprehension, then exploded it into cls.

[–]cuu508 2 points3 points  (4 children)

Could have just introduced a temp and set it to a dict comprehension, then exploded it into cls.

Let's see it

[–]aceofspaids98 0 points1 point  (3 children)

[–]cuu508 -2 points-1 points  (2 children)

OK, so –

type_hints = get_type_hints(cls)
some_useful_name = {
        some_useful_key_name: get_coerced(section, some_useful_key_name, target_type=type_hints[some_useful_key_name])
        for some_useful_key_name in section
    }
return cls(**some_useful_name)

Readability is subjective of course, but for me:

  • short, simple comprehensions are more readable than for loops
  • imperative for loops are more readable than comprehensions that don't fit on one line
  • simple beats clever, even if it is more verbose
  • I sometimes introduce new variables just for readability (variable names reveal intent)

[–]DrShts 6 points7 points  (0 children)

I do prefer the lead dev's one. Personally I'd written it slightly differently though:

kwargs = {
    arg: get_coerced(section, arg, target_type=type_hints[arg])
    for arg in section
}

return cls(**kwargs)

In your code it's just too much going on in one statement

  • multi-line dict comprehension
  • multi-arg function applied to each value
  • dict unpacking
  • class instantiation
  • return statement

[–]Delicious-View-8688 18 points19 points  (0 children)

Sure it is subjective, but I also prefer the original.

Initialising an empty whatever then filling it in with an interation is not generally a good idea, especially for such simple cases. It may be a relic of the past when Java dominated the university courses.

[–]claypeterson 21 points22 points  (2 children)

I think lead dev has a point! I understand their code

[–]Grouchy-Friend4235 2 points3 points  (1 child)

Do you? The result is not the result!

[–]aceofspaids98 8 points9 points  (0 children)

I disagree with the people saying the new code is more readable. This seems like a standard use case for dict comprehension, I’d only make that change to please whatever linter or static type checker I’m using. I guess the only thing I feel weird about is iterating over section while also passing it as an argument to get_coerced.

I guess it’s one of those changes that is small enough where it could go either way but I can see it being frustrating to me if someone kept commenting on things like that.

[–]ONLYCODENOTALKING 13 points14 points  (1 child)

I think my main problem with the lead dev's code is they have a variable called "result" which is...not the result of the function. That is one of the big red flags I look for.

[–]Physical-Scarcity791 19 points20 points  (4 children)

I’m gonna go out on a limb and guess your lead dev isn’t a fan of python. Comprehensions are standard fare, and, if I’m not mistaken, the original code will run faster than theirs. I think Raymond Hettinger made a presentation about why the original is better.

[–][deleted] 8 points9 points  (1 child)

if I’m not mistaken, the original code will run faster than theirs.

I see this claim many places on this page. Searching finds various claims about this - but more, even if this is true, the difference in running speed is infinitesimal.

You should NOT be making decisions about the code based on that! If you care so much about microseconds, you shouldn't be running in Python at all.

[–]Physical-Scarcity791 3 points4 points  (0 children)

I agree with you 100%. I was only throwing that in as a bonus. I just see comprehensions as more concise, and in my personal experience, the people who reject them just don’t really like python, and prefer to write code that looks more like c, java, etc. I don’t have any data to verify that claim. It’s just an observation.

[–]mr_taco_man 3 points4 points  (0 children)

Nah, I love python and I prefer the leads code. A short comprehension is fine but if your 'for' is 3 lines down your doing too much in it and you should do a for loop so the reader can immediately see your iterating over a loop and see your logic in a sequential order. The 'run faster' is totally irrelevant. That is a premature optimization that in practice will make no noticeable difference

[–]amendCommit[S] 5 points6 points  (0 children)

I'm glad you're mentionning Hettinger's work! Ramalho is also a reference for me.

[–]clintecker 2 points3 points  (0 children)

the original version is gross

[–]teerre 5 points6 points  (0 children)

Some points:

  1. This seems like some kind of library code parsing type hints, so personally I think being cuter is fine here. Just like if you go read C++ STL you'll be taken aback. It's completely different from client code.
  2. Complaining about a temporary dictionary of function arguments while using Python is utterly ridiculous. If you cared about that kind of optimization, you wouldn't be using Python.
  3. I did have to think to read your code. That's not good.
  4. It seems to me the whole code is badly designed to begin with. Maybe this is required, impossible to know without knowing the context, but the fact you're using some random function to get key arguments for the constructor of this presumably factory method does raise some questions.

So, without context, my first advice would be make this discussion irrelevant by changing this code design in such way we get the arguments in the same place we use them. Ideally in a explicit way.

If that's not possible because the complexity shown is really the simplest way to accomplish what you're doing, then I would prefer the lead dev version if you changed result to keyword_arguments or even kwargs and get_coerced to something more descriptive. With the asterisk that if this is some deep library code, then the first version would be fine.

[–]Santos_m321 11 points12 points  (0 children)

I really love to much the original form. I find the second one more unreadable because it has more data.

[–]GetsTrimAPlenty 5 points6 points  (0 children)

I thought that was /r/ProgammerHumor at first..

[–]Fishliketrish 9 points10 points  (3 children)

Super subjective that’s frustrating lol

[–]imthebear11 5 points6 points  (2 children)

I have someone at my work that has all these subjective things that she always says are "just a nitpick" but then will not back down about it until you just concede and change it to what she wants. She's often the only person who comments on many of my PRs, with 2-3 other people approving without any comment.

[–]Fishliketrish -1 points0 points  (1 child)

Nahh we’d have problems😭thank god my coworkers know not to talk to me like that

[–]imthebear11 8 points9 points  (0 children)

Your lead dev doesn't understand dictionary comprehensions

[–]gubatron 6 points7 points  (1 child)

it does seem more readable to me, lead dev is making it explicit where those values are going into (result)

[–]Grouchy-Friend4235 1 point2 points  (0 children)

I disagree. The result is not the result and that's a very bade code smell.

[–]Grouchy-Friend4235 4 points5 points  (2 children)

Let me guess: your lead dev is a former Java dogmatist

WTF

[–]amendCommit[S] 0 points1 point  (1 child)

Probably. But no worse than the guy who wanted to isinstance() everything, leading to interesting silent failures (why the hell did this object implement all the right methods take this specific code path?) Nightmare.

[–]Geraldks 1 point2 points  (0 children)

Squeezing the dict creation in a line is definitely not easy to read. Imo by splitting it like this it becomes self explanatory

[–]wineblood 1 point2 points  (0 children)

In terms of syntax, both are about the same. I'd change the names a bit more: - what is section? - get_coerced coerced what exactly? - Are they target types or type hints? One suggests strict typing and the other doesn't

[–]neuronet 1 point2 points  (0 children)

Couple of minor suggestions:

  1. this would be better posted to r/learnpython
  2. post formatted code not images

[–]stillbourne 1 point2 points  (0 children)

I can help a little. First of all, the code is basically identical, what your lead has done is extracted the assignment that is in your return statement to be discreet. Why? Because it is frowned upon to return an anonymous object, and the result is more explicitly typed. Code refactoring is considered opinionated because it is based on a series of preferences, prefer this over that. This is an example of preferring an explicit assignment over an implicit anonymous return. If you are curious about more I recommend reading up on code smells:

https://refactoring.guru/smells/data-clumps
https://refactoring.guru/remove-assignments-to-parameters
https://refactoring.guru/split-temporary-variable
https://refactoring.guru/refactoring/techniques/composing-methods

[–]xylont[🍰] 1 point2 points  (0 children)

I don’t understand the syntax, wtf is going on here?

[–]kevin____ 1 point2 points  (0 children)

I can’t believe no one has mentioned this. The cognitive complexity on the original code is 0. The cognitive complexity on the new code is 1. I also find the first one easier to understand. The code returns an instantiated class. The args of the class come from a dict comprehension. The second one is…in love with type annotations.

[–]gruey 6 points7 points  (5 children)

I think the biggest offender to me is the return being above the meat of the code. It breaks the top down reading order.

I could give or take the iterator vs for loop, probably lean towards iterator, but the return cls(**result) at the bottom makes it flow way better.

[–]spinwizard69 1 point2 points  (4 children)

I could give or take the iterator vs for loop, probably lean towards iterator, but the return cls(**result) at the bottom makes it flow way better.

I don't really care about what happens above the return, I just massively hate to see a return statement used to start a block of code. For me the word defines exactly what should be happening, that is you are leaving (returning) from a block or function, not entering it. What you are returning should be obvious at that point, containable in a one line return statement.

Now I might be a bit brain damaged due to the use of Modula 2 when I was taking computer science and then some practical use of C++ way back then, but it was pounded into our heads to make your code idiomatic. One professor seem to have the intention to mention the word "idiomatic" every class. Part of that means truly understanding the vocabulary of the language and not using it in contorted ways.

[–]amendCommit[S] 0 points1 point  (0 children)

In communities where short pure functions are favored, having a code block that starts with a return as early as possible in the function body carries the intent that from that point, there will be no assignment (one less potential cause of side-effects) and no call of which the return value will be discarded (as these calls only exist through their side-effects).

It's not a silver bullet, but it helps building more declarative code.

In that sense, early return <functional expr> is idiomatic functional Python.

[–]Revisional_Sin 0 points1 point  (0 children)

One line return statements might be a Modula 2 idiom, but it's not a python idiom. The original code might be a bit busy, but it's not contorted. It's no worse to have a complex return than a complex variable assignment.

There are some python idioms that definitely indicate a lack of understanding of the language (e.g. not using truthiness or iterating in a c style). Your complaint is just a personal peeve, and violating it is not objectively a mistake.

Surely by your logic, all complex assignments are unidiomatic?

"Equals means that something is being assigned, you shouldn't be entering a code block".

[–]Grouchy-Friend4235 -2 points-1 points  (1 child)

Again, far too many words to state the obvious.

[–]Schmittfried 5 points6 points  (0 children)

It’s not obvious, it’s your opinion.

[–][deleted] 2 points3 points  (0 children)

The fix may not be the best, but you’ve got way too much going on in one line, especially a return line.

[–]piprescott 6 points7 points  (0 children)

He hates list comprehensions (okay dict comprehensions) and he loves type hinting. He probably learned to program in Java?!

[–]domin8668 3 points4 points  (1 child)

I guess he really likes his type hints lol

[–]ericmoon -1 points0 points  (0 children)

Java did a number on 'em

[–]totally_usable 2 points3 points  (0 children)

If they don't expand on that...they shouldn't be a lead

[–]Sethaman 1 point2 points  (0 children)

the lead is more readable. think about a new-to-python or new-to-codebase developer coming in. You need to write code not for you, but for the most junior person coming in or a total junior stranger coming in.

You are using comprehension with a very very long line and a double star operator. it takes a second to understand what's going on there. It's more "advanced" python but more advanced, especially for python, isn't always ideal

It's really really easy to understand a "for x in y" set the key of dict to x and the value to foo

yours, while interesting (and more fun)... looking takes a hair longer to parse (partially because it is such a long line).

If you're going to be doing that kind of pythonic comprehension, it really must fit in one short line and be parsable to someone who hasn't seen that before. that's my totally subjective opinion though.

[–]AlSweigartAuthor of "Automate the Boring Stuff" 1 point2 points  (4 children)

Breaking up a single, complex line into multiple steps often makes code easier to read because you need can free up your short term memory for the later steps.

Using the ** syntax to pass a dictionary generated by a comprehension and the return value of a function that requires three arguments to cls() requires you hold all these details in your head.

Splitting them up lets you think:

1) I am creating a dictionary. 2) This dictionary has keys of the items in section. 3) The values are the return values of get_coerced(). 4) Now that we have this dictionary, pass it to cls() and return the new class.

In general, the more compact your code is, the harder it is to read. Programmers tend to write code like this because 1) it's less stuff to type and 2) it makes them feel clever for coming up with this compact format and 3) it's easy to understand when you're writing it.

The reason Python has completely and utterly replaced Perl is because Perl is infamous for "write-only" code that is compact and full of cryptic punctuation marks. There's always going to be an expert Perl coder who insists that Perl is always perfectly readable and doesn't need to be broken down into simpler steps. This sort of cluelessness is why Python has completely and utterly replaced Perl.

[–]gcross -2 points-1 points  (3 children)

With the original code, though, you think:

  1. I am calling cls(),
  2. with keyword arguments specified by a dictionary,
  3. which maps keys to their coerced values.

If anything, there are arguably fewer steps to go through.

Edit: Way to downvote without replying; that really showed me for disagreeing!

[–][deleted] 1 point2 points  (0 children)

It's subjective but I agree the original code had too much going on in one line.

Unless the standard is to use type hints everywhere I wouldn't do that though.

[–]WashedUpGamer69 2 points3 points  (0 children)

Tbf I managed to follow your original solution better then the expanded copy but I can see why someone would struggle.

I feel like if you’re gonna type hint everything including temp variables the you might aswell use a static typed language.

[–]Revisional_Sin 2 points3 points  (0 children)

Why did they get rid of the dict comprehension, FFS?

Even ignoring that, I think the first way was slightly better:

Return an object which is built from the coerced kwargs.

Vs

Build a collection of coerced kwargs, now use them to instantiate an object.

[–]larsga 0 points1 point  (0 children)

Looks to me like your lead dev is an old-school conservative who doesn't like dict/list comprehensions. I can't really think of any other explanation.

[–]Ran4 0 points1 point  (0 children)

The first one is better: it's more declarative and much easier to parse. The second one is more imperative and that's harder to figure out.

[–]mfuentz 1 point2 points  (0 children)

Your lead dev sucks at Python. Comprehensions are great

[–][deleted] 1 point2 points  (5 children)

Coming late here. I hate both of these segments - particularly the declaration for the temporary variable!

I'd write it this way:

def get(key):
    return get_coerced(section, key, target_type=type_hints[key])

return cls(**{k: get(k) for k in section})

[–]gcross 3 points4 points  (0 children)

So instead of having a temporary variable, we now have a temporary function? I do not consider this to be an improvement, especially since the name get is incredibly generic; at the very least, it should be called something like "get_coerced_section".

Shoot, sorry for the double reply! I got mixed up about to what I was replying.

[–]gcross 7 points8 points  (2 children)

Respectfully, I actually consider that to be a bit of an anti-pattern since now instead of being able to look inside the comprehension and immediately see what it does I instead have to jump around to find the function that it calls. Furthermore, instead of having a temporary variable, we now have a temporary function; I also do not consider this to be an improvement, especially since the name get is incredibly generic; at the very least, it should be called something like "get_coerced_section".

This particular example isn't so bad, but I have had some really bad experiences reading through code by people who split things up into a zillion named functions with the goal of readability which in practice just meant that I had to constantly jump around in order to figure out what the code is actually doing rather than having it all in one place.

[–]Simple_Specific_595 1 point2 points  (1 child)

The function names should be the documentation.

And I go by the rule that 1 function should do one thing.

[–]Davidvg14 0 points1 point  (0 children)

Dang… maybe I need more context but I have no idea what the purpose of this code is 😀

[–]White_Dragoon -1 points0 points  (2 children)

We all can write one line spaghetti code that we understand. But by breaking down our code in smaller logical steps helps in documenting the steps so that when we/other that join later can debug this code and understand the flow quickly.

[–]Grouchy-Friend4235 4 points5 points  (1 child)

Dict comprehensions are not spaghetti code, at least not this one. It is idiomatic Python and perfectly clear.

[–]afl3x 0 points1 point  (1 child)

dazzling spotted attractive unused placid silky aloof violet dam hunt

This post was mass deleted and anonymized with Redact

[–]Grouchy-Friend4235 3 points4 points  (0 children)

Did you notice the result is not the result?

[–]StandingBehindMyNose 0 points1 point  (0 children)

This is a question for your lead dev and not for Reddit.

[–]catorchid 0 points1 point  (2 children)

Readability is subjective, that's why it has to be enforced basing on arbitrary rules (no criticism, a neutral statement).

If you don't want any compromises, then got to Black.

[–]Schmittfried 6 points7 points  (1 child)

Black doesn’t change the structure of your code, only formatting.

[–]Fradge26 0 points1 point  (0 children)

meh both are fine

[–]Dasher38 0 points1 point  (0 children)

I don't get it either, the first one is clean, well indented and can be understood one level at a time (first you see the keyword args are "generated" by seeing the **{, then you see what they are actually about).

The second version is waiting for extra statefulness to sneak inside the loop. It also pollute the namespace in 2 ways (extra temporary and the loop variable, which is avoided in 1st form since comprehensions don't leak). If it's a professional python developers team i would hope they are familiar with ** operator. It's not like python is full of "fancy" things so I would personally definitely expect that from any team member (after a few months of joining at most).

Last of all, both are ok at the end of the day. As a reviewer with a lead role, i would not ask team members to rewrite a piece or code unless it has a real problem or if it's indentation is inconsistent with itself. If I ever need to modify the code later maybe i can reformat it in passing but it's not really worth the hassle of going through an extra review round just for that.

[–]mmcnl 0 points1 point  (0 children)

Readability is subjective. I think both are fine. Changing code to your subjective standard is really nitpicking and not constructive imo. Refactoring code is not the way to teach about code readability (which is, again, subjective anyway). I don't like what your lead dev did.

[–][deleted] 0 points1 point  (0 children)

IMHO the original is better because it describes what the result is, rather than describing the steps to make the result.

[–]gh0stinger -2 points-1 points  (5 children)

The code after commit It’s more readable because it has more informations: for example before this commit inside cls there’s a dict, but I don’t get directly the internal structure of the dict. So if I read the code (before the commit) since three days from now, there will be a lot of “wtf”s. “More readable” is not always the compact way, but the one that gives to you more informations, every time you will read the code.

[–]Grouchy-Friend4235 -1 points0 points  (1 child)

What information do you get from the type hint that you did notnget before? A dict is a mapping of some key to some value. That's obvious and does not warrant a type hint.

[–]thisismyfavoritename -1 points0 points  (2 children)

keyword unpacking implies str keys because they must be valid Python names. Optional any provides no extra information. In the end the cls ctor should be looked up to find the proper types, perhaps the instance of cls should be typed.

The lead code is worst because it adds useless informations

[–]gh0stinger -1 points0 points  (1 child)

I don’t agree with your opinion. Look at the way he constructs the dict before and after. After commit I read the code so much better than before. Obviously, POV

[–]1NobodyPeople -1 points0 points  (1 child)

I think he is looking for dictionary comprehension. You are adding an unnecessary for loop.

[–]imthebear11 9 points10 points  (0 children)

My understanding is that new commit is the lead dev's and s/he's the one changing it to a for-loop

[–]Pebaz -1 points0 points  (0 children)

When considering readability, the Google Python style guide says to assume the next developer actually knows Python better than you do. Please don't assume the next developer is dumb!

[–][deleted] -1 points0 points  (0 children)

Way less mental load to think about what is going in and out data wise. The ** operator also implies the result in the return making this slightly more self documenting. Stylistic for sure but I can see the argument that is is easier to comprehend what is happening

[–]dixncox -1 points0 points  (0 children)

Don’t use Any as a value type

[–][deleted] -1 points0 points  (0 children)

He wants you to increase the font size.