The ability of **kwargs to leave behind no proper documentation and silently swallow any invalid arguments has made us remove them entirely from our codebase. They're almost entirely redundant when you have dataclasses.
What about decorators, or wrappers around third-party code whose contracts change frequently (or even second party code when interacting with functions provided by teams that don't follow explicit argument typing guidelines, if you have that sort of culture)?
Usually the solutions range from a culture of “just don’t” to tests/mypy that have become increasingly stricter over the years, every time we’ve come a step further up the ladder. But I admit, it has taken quite some bridging to get there.
Moving to static Python in most places has dramatically improved the code and language.
For everybody reading this and scratching their head why this is relevant: Python subclassing is strange.
Essentially super().__init__() will resolve to a statically unknowable class at run-time because super() refers to the next class in the MRO. Knowing what class you will call is essentially unknowable as soon as you accept that either your provider class hierarchy may change or you have consumers you do not control. And probably even worse, you aren't even guaranteed that the class calling your constructor will be one of your subclasses.
Which is why for example super().__init__() is pretty much mandatory to have as soon as you expect that your class will be inherited from. That applies even if your class inherits only from object, which has an __init__() that is guaranteed to be a nop. Because you may not even be calling object.__init__() but rather some sibling.
So the easiest way to solve this is: Declare everything you need as keyword argument, but then only give **kwargs in your function signature to allow your __init__() to handle any set of arguments your children or siblings may throw at you. Then remove all of "your" arguments via kwargs.pop('argname') before calling super().__init__() in case your parent or uncle does not use this kwargs trick and would complain about unknown arguments. Only then pass on the cleaned kwargs to your MRO foster parent.
So while using **kwargs seems kind of lazy, there is good arguments, why you cannot completely avoid it in all codebases without major rework to pre-existing class hierarchies.
For the obvious question "Why on earth?"
These semantics allow us to resolve diamond dependencies without forcing the user to use interfaces or traits or throwing runtime errors as soon as something does not resolve cleanly (which would all not fit well into the Python typing philosophy.)
FWIW, I've come to regard this (cooperative multiple inheritance) as a failed experiment. It's just been too confusing, and hasn't seen adoption.
Instead, I've come to prefer a style I took from Julia: every class is either (a) abstract, or (b) concrete and final.
Abstract classes exist to declare interfaces.
__init__ methods only exist on concrete classes. After that it should be thought of as unsubclassable, and concerns about inheritance and diamond dependencies etc just don't exist.
(If you do need to extend some functionality: prefer composition over inheritance.)
At an even more basic level, the lack of static typing seems like such a tradeoff getting an incredibly huge nuisance in readability and stupid runtime bugs that shouldn't be a thing in exchange for a feature that's rarely useful.
Granted, I'm primarily an embedded developer. Can any Python experts explain to me a highly impactful benefit of dynamic typing?
For small programs, dynamic typing can be faster to write (not read). As soon as your program grows: "uh oh". Once you add maintenance into the cost equation, dynamic typing is a huge negative.
To be fair: 15 years ago, people were writing a lot of Java code that effectively used dynamic typing by passing around Object references, then casting to some type (unknowable to the reader) when using. (C#: Same.) It was infuriating, and also very difficult to read and maintain. Fortunately, most of that code is gone now in huge enterprise code bases.
I'm not sold on this. Often I type the output I want to get, and reverse the code to get there. and that's faster because it's now all auto completing.
That's been my experience of powershell and typescript. To a lesser extreme python because its type hints are a bit crap.
Though I can see why you might not agree after trying an extreme like Rust. Sometimes I want to run a broken program to stop the debugger and see what I'm dealing with and rust won't do that.
I strongly disagree. Not converting the int to a string automatically is absolutely the right decision. In all code I write, this TypeError would catch an actual error, because concatenation of strings is just not the right tool for creating "abc123" from "abc" and 123, so I would not use it for that. Hence, if this exception occurs, it indicates that I probably mixed up variables somewhere. Use one of the (admittedly too) many string formatting tools that Python offers, for example an f-string like f"abc{123}". (Also, if you have enough type annotations in your code, the type checker will warn you about these, so you can fix them before they hit testing or even production.)
Interesting. 100% of the times I encountered this TypeError, I actually wanted to create the concatenated string. It never caught an actual error.
Now, I guess I'm not against and explicit cast and I can imagine how the error could catch an actual bug. It's painful when the error stops the execution when the string concatenation was intended, but it is not really an issue anymore with the possibility to type check before the execution.
> concatenation of strings is just not the right tool for creating "abc123" from "abc" and 123
Why? This sounds like an opinion to me. String interpolation of formatting features are nice but I find them quite clunky in such simple cases.
Of course when you have to be careful to call str(val), it's arguably as clunky...
Of course it’s an opinion. But in my experience, I almost exclusively have some sort of “template” with some parts to fill out, and string interpolation represents this better (in my opinion). This is especially true if the different parts to fill in are of different types.
As I wouldn’t use string concatenation for this purpose, it’s impossible for me to run into a situation where I wanted the concatenated string. (And even if I did, I would be glad for the reminder to change this into an f-string.)
And the bugs that it catches are of the form: I took some user input, expecting it to be a number, but forgot to convert it into one. Then I passed it to a function expecting a number, and it thankfully crashed instead of turning everything else into strings as well.
Maybe this is also a question that informs your view on this: Do you expect "abc" or 123 to be the “variable” part of that expression?
- If "abc" is a literal in the code with 123 coming from a variable, wanting 123 to turn into a string as well is somewhat unterstandable.
- However, if 123 is the literal part of the code and "abc" the value of some variable, I would expect to mostly run into this in cases where I am actually doing some math and that the variable is a string just is some accidentally unparsed input.
In what I do, the second case would be more common.
> So the easiest way to solve this is: Declare everything you need as keyword argument, but then only give *kwargs in your function signature to allow your __init__() to handle any set of arguments your children or siblings may throw at you. Then remove all of "your" arguments via kwargs.pop('argname') before calling super().__init__() in case your parent or uncle does not use this kwargs trick and would complain about unknown arguments. Only then pass on the cleaned kwargs to your MRO foster parent.
The easiest way is to not put your arguments into kwargs in the first place. If you put them as regular function arguments (probably give them a default value so they look like they're related to kwargs), then the python runtime separates them from the rest when it generates kwargs and you don't have to do the ".pop()" part at all.
Thank you for explaining this; there are a lot of comments here suggesting trivial code style improvements for use cases where *kwargs wasn’t actually needed. The more interesting question is how to improve the use case you describe — which is how I’ve usually seen *kwargs used.
Are traits and mixins the same? If not, can you please provide a trivial example. It would be useful to better understand what you mean. When I was very young, learning C++, I thought multiple inheritance was so cool. Now, I know it is like sleeping in the open jaws of a saltwater croc.
Traits as in the original Smalltalk research, Rust traits or Haskell type classes are like interfaces, but only when in scope. So until you import a trait, its implementation on various types isn’t visible.
This makes it possible to safely make a new trait and implement it on a built in type (like giving int a method) without the chance of another unrelated use of the type accidentally using what this trait provides.