[HN Gopher] Python Type Hints - *args and **kwargs (2021)
       ___________________________________________________________________
        
       Python Type Hints - *args and **kwargs (2021)
        
       Author : ekiauhce
       Score  : 189 points
       Date   : 2023-08-27 13:11 UTC (9 hours ago)
        
 (HTM) web link (adamj.eu)
 (TXT) w3m dump (adamj.eu)
        
       | curiousgal wrote:
       | Is it just me or are Python type hints like..goofy?
        
         | PheonixPharts wrote:
         | As someone who has written Python for nearly 20 years now, and
         | also has plenty of experience with strongly and statically
         | typed languages (including a fair bit of Haskell), I think type
         | hints in Python should at most remain just that, _hints_.
         | 
         | A language being statically typed or dynamically typed is a
         | _design_ decision with implications for what the language can
         | do. There are benefits to each method of programming.
         | 
         | Trying to strap type checking on to Python is born out of some
         | misplaced belief that static type is just better. Using Python
         | as a dynamically typed language allows you for certain
         | programming patterns that cannot be done in a statically typed
         | language. There are some great examples in SICP of Scheme
         | programs that could not exist (at least with as much elegance)
         | in a typed language. Dynamic typing allows a language to do
         | things that you can't do (as easily/elegantly) in a statically
         | typed language.
         | 
         | Some may argue that these type of programming patterns are
         | _bad_ for production systems. For most of these arguments I
         | strongly _agree_. But that means for those systems Python is
         | probably a poor choice. I also think metaprogramming is very
         | powerful, but also a real potential footgun. It would be
         | ridiculous to attempt to strip metaprogramming from Ruby to
         | make it  "better", just use a language that depends less on
         | metaprogramming if you don't like it.
         | 
         | This is extra frustrating because in the last decade we've seen
         | the options for well designed, statically typed languages
         | explode. It's no longer Python vs Java/C++. TypeScript and Go
         | exist, have great support and are well suited for most of the
         | domains that Python is. Want types? Use those languages.
        
           | mirsadm wrote:
           | Have to disagree with this. Choosing a language is not just
           | about its features but also its ecosystem. I chose Python for
           | my current project because it has great libraries that don't
           | exist in other languages.
        
             | PheonixPharts wrote:
             | "my current project" type problems are where Python is
             | great. Types remain "nice to have" (if you like them) and
             | aren't really essential compared to the ease of prototyping
             | new ideas and building a PoC. You're choosing Python
             | because the benefit of libraries outweighs your personal
             | preference for types.
             | 
             | Most of my work is in machine learning/numeric computing,
             | so I'm very familiar with the benefits of Python's
             | ecosystem. Basically all of AI/ML work is about prototyping
             | ideas rapidly, where access to libraries and iterating fast
             | greatly trumps the need for type safety.
             | 
             | At nearly every place I've worked, Python is the tool for
             | building models quickly but shipping them to production and
             | integrating them with the core product almost always
             | involves another language, typically with types, better
             | suited for large engineering teams working on a large code
             | base where you _really_ want some sort of type checking in
             | place. Most of the companies I know that do serious ML in
             | production typically take models from python and then
             | implement them in either C++ or Scala for the actual
             | production serving.
             | 
             | It's worth pointing out that the vast majority of those
             | libraries you use were initially developed without any
             | consideration, or need, for types. Great, reliable,
             | software can be written without types. Dynamic typing is a
             | great language choice, and there's no need to fight the
             | language itself by trying to bolt types on.
             | 
             | Where types are important is where you have a complex,
             | rapidly changing code base with a large number of
             | developers of differing skill levels releasing frequently.
             | If that's the environment you're in, I would _strongly_
             | recommend against using Python in prod, even if it means
             | you have to implement the features of some libraries
             | internally.
        
         | insanitybit wrote:
         | They're quite limited in some ways, obscenely powerful in
         | others, and have a fairly strange syntax, yeah.
        
         | jcalvinowens wrote:
         | I agree. It adds all the inconvenience of static typing with
         | none of the benefits.
        
         | lijok wrote:
         | Big time. Getting better very quickly however
        
         | b5n wrote:
         | Call me crazy, but I just use a statically typed language where
         | static types are required.
        
       | m3047 wrote:
       | That article promulgates a misunderstanding about immutability.
       | For my way of thinking, python is already an interpreted language
       | and I can enforce tropes in code more cleanly and effectively
       | than people taking something five levels up at face value and
       | trying to figure out what sticks when they throw it against the
       | wall: no wonder they end up frustrated, and it's a frustrating
       | situation.
       | 
       | Given:                   def foo(*args):             print(args)
       | return              class Thing(object):             def
       | __init__(self,a,b):                 self.a = a
       | self.b = b                 return                          def
       | foo_style(self):                 return (self.a, self.b)
       | 
       | _args is not required to refer to a tuple:                   >>>
       | foo(*[31,42])         (31, 42)
       | 
       | I can have objects construct parameters conforming to the
       | specifications for a signature:                   >>>
       | foo(*Thing(3,91).foo_style())         (3, 91)
       | 
       | Consider that a counterexample._
        
         | adamchainz wrote:
         | Within the function, args is a tuple, as your output
         | demonstrates.
        
         | [deleted]
        
       | dfee wrote:
       | I actually created a library for this!
       | 
       | Forge: forge (python signatures) for fun and profit
       | 
       | https://python-forge.readthedocs.io/
       | 
       | https://github.com/dfee/forge
        
       | assbuttbuttass wrote:
       | This does restrict all of your keyword arguments to the same
       | type. If you have keyword arguments of different types, you're
       | right back to no type safety.
        
         | actualwitch wrote:
         | Well, if you want to type your kwargs and use newer versions of
         | python, you can use Unpack with typed dicts to achieve that.
         | But the footgun there is that you can't redefine fields when
         | extending them, so no Partial<SomeType> for you.
        
         | zbentley wrote:
         | True, but there are a couple of mitigations available: you can
         | express the types of selected kwargs (by leaving them out of
         | the * residual), and you can use typing.Union/| to express
         | product types for values in the residual as well.
        
         | masklinn wrote:
         | That seems obvious? If you want a variable number of arguments
         | of arbitrary type you have to specify the common supertype,
         | commonly top itself.
         | 
         | To do otherwise would require some form of vararg generics
         | which is uncommon.
        
           | IshKebab wrote:
           | It's extremely common for Python programmers to write code
           | with kwargs of different types. Look at subprocess.run() for
           | example.
        
       | PartiallyTyped wrote:
       | Alternatively, use an `@overload` in a `.pyi` file and specify
       | your types there.
       | 
       | This means that you will have 2^N combinations and doubling every
       | time you accept a new argument.
       | 
       | If that is not good enough, then simply use a `TypedDict` with
       | everything optional instead of `**kwargs`. Your call will then
       | become `foo(SomeTypedDict(p1=p2,...))`.
        
       | amelius wrote:
       | What if the second argument is a float?
        
       | vorticalbox wrote:
       | Why do people not just type everything they want passed?
       | 
       | def variable(n:str, nn:str, nnn:str, *, a:int, b:int, c:int)
       | 
       | Anything after,*, is a kwarg.
        
         | Frotag wrote:
         | It's pretty common when wrapping a function that has a large
         | number of config options.
         | 
         | The wrapper is usually some shorthand for building a handful of
         | those args or adding some side-effect, while still allowing the
         | caller access to the remaining config options via kwargs.
         | 
         | Here's one example of that in the wild
         | https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot....
        
         | IshKebab wrote:
         | In my experience it's generally because Python developers make
         | functions with an insane number of keyword arguments, and then
         | wrap those functions. They don't want to type them all out
         | again so they use kwargs.
         | 
         | subprocess.run() is an example of that. Also check out the
         | functions in manim.
         | 
         | The inability to properly static type kwargs using TypedDict is
         | probably the biggest flaw in Python's type hint system (after
         | the fact that hardly anyone uses it of course).
        
         | jpc0 wrote:
         | If you have enough arguments that the signature becomes obscure
         | to read you need a dataclass to pass into the function instead.
         | 
         | I would rather:                   @dataclass(frozen=True,
         | slots=True)            class VarThings:                n: int
         | ...              def variable(a: VarThings):                ...
         | 
         | Than a million args
        
           | b5n wrote:
           | I usually start with a namedtuple unless I need the
           | additional features provided by a dataclass.
        
             | joshuamorton wrote:
             | Why? Dataclasses are vastly better: more typesafe, less
             | hacky, etc.
        
         | masklinn wrote:
         | Your signature requires exactly 3 positional[0] and 3 keyword
         | arguments. The OP allows any number of either.
         | 
         | [0] actually 3 positional-or-keyword which is even more widely
         | divergent
        
           | vorticalbox wrote:
           | But why would you want that doesn't that make for a more
           | confusing api? Would it not be better to just have everything
           | as a kwarg? You would get better types that way
        
             | masklinn wrote:
             | I genuinely don't understand what you are asking.
        
             | zbentley wrote:
             | I think what GP is saying is that with explicit kwargs you
             | can't express _variadic_ signatures, i.e.  "this function
             | takes one int positional, and then _any number of_ key
             | /value pairs where the values are lists". The variable
             | length is the important bit.
             | 
             | It's certainly debatable whether doing that is better than
             | passing a single argument whose value is a dict with that
             | same type, but many people do prefer the variadic
             | args/kwargs style.
        
         | Znafon wrote:
         | It is used when the number of argument can vary, like:
         | def sum(*args: int) -> int:             if len(args) == 0:
         | return 0             return args[0] + sum(*args[1:])
        
           | zoomablemind wrote:
           | It seems altogether surprising that with an empty list or
           | tuple a, a[1] results in index error, yet a[1:] quietly
           | returns an empty list or tuple.
        
             | hk__2 wrote:
             | > It seems altogether surprising that with an empty list or
             | tuple a, a[1] results in index error, yet a[1:] quietly
             | returns an empty list or tuple.
             | 
             | `a[1:]` returns the sequence of elements that start at
             | index 1. If there is no such element, the list is empty. I
             | don't see any good reason why this should throw an error.
        
               | macintux wrote:
               | Then why doesn't a[1] return None?
               | 
               | I understand the logic behind both decisions, but it's
               | not surprising that people find it inconsistent and
               | unintuitive.
        
               | hk__2 wrote:
               | > Then why doesn't a[1] return None?
               | 
               | Because there would be no way to distinguish between
               | "a[1] contains None" and "a[1]" doesn't exist.
        
               | macintux wrote:
               | And with a[1:] returning the empty list there's no way to
               | distinguish between a is empty and a only has one
               | element.
               | 
               | These are, in the end, relatively arbitrary language
               | design decisions.
        
               | akasakahakada wrote:
               | When you slice a list, you get a list. When you see there
               | is nothing inside the returning list, you know that means
               | end of list, contains zero element. Slicing and indexing
               | return object at different level.
        
               | macintux wrote:
               | Slicing a list, when the first index is invalid for that
               | list, could easily throw an exception instead.
        
               | zoomablemind wrote:
               | This should signal an explicit error, which invalid index
               | is indeed. If user believes for some reason the invalid
               | indexing is ok, then it could be caught and handled. No
               | ambiguity.
        
               | Znafon wrote:
               | I think it is consistent, it works a bit like filtering
               | an element from a mathematical set.
               | 
               | Given a set of sheeps, let x be the five-legged sheep is
               | inconsistent because we know neither the existence or
               | uniqueness of shuch sheep, so it raises an exception.
               | 
               | Given a set of sheeps, let x be the subset of five legged
               | sheeps is the empty set because there is no such sheep.
               | 
               | but this may also just be because I internalised Python's
               | behavior.
               | 
               | Some language have a specific value to denote the first
               | thing, for example:                  ["a", "b", "c"][4]
               | 
               | gives `undefined` in JavaScript but it differs from
               | `null` which would be the equivalent to `None` in Python
               | (and I don't think Python has such concept).
        
               | zoomablemind wrote:
               | Both cases are an index error. It's just for some other
               | reasons in case of the section, the error is represented
               | by an empty object and it's left to user to handle the
               | result.
               | 
               | This could easily conceal the indexing error unless the
               | caller code explicitly checks the length of the returned
               | section.
        
             | js2 wrote:
             | a[1] has to raise an IndexError because there's no return
             | value it could use to otherwise communicate the item
             | doesn't exist. Any such value could itself be a member of
             | the sequence. To behave otherwise, Python would have to
             | define a sentinel value that isn't allowed to be a member
             | of a sequence.
             | 
             | When using slice notation, the return value is a sequence,
             | so returning a zero-length sequence is sufficient to
             | communicate you asked for more items than exist.
             | 
             | It may be surprising, but it almost always leads to more
             | ergonomic code.
             | 
             | https://discuss.python.org/t/why-isnt-slicing-out-of-range/
        
           | esafak wrote:
           | You should use `Iterable`
        
             | Znafon wrote:
             | I'm not sure                   print(firstname, lastname)
             | 
             | for example is more readable than
             | print((firstname, lastname))
             | 
             | especially since I would then have to write
             | print((surname,))
             | 
             | to just print a single string.
             | 
             | Variadic functions are rather classic, I think Go, Rust, C
             | and JavaScript also have them.
        
               | uxp8u61q wrote:
               | How is it more "readable"? The two are just as readable.
               | 
               | What do you do with your first example if you have a list
               | (generated at runtime, not a static one) to pass to the
               | function? This wouldn't work (imagine the first line is
               | more complicated):                   l = (1,2,3)
               | print(l)
        
               | insanitybit wrote:
               | FWIW Rust does not have variadic functions. The closest
               | thing would be either macros, which are variadic, or
               | trait methods, which are not variadic but can look like
               | they are.
        
               | Znafon wrote:
               | Oh yeah, that's right! Thanks for the correction
        
               | esafak wrote:
               | Your example has a fixed number of names. What if you
               | wanted to accept any number of names, like _Pablo Diego
               | Jose Francisco de Paula Juan Nepomuceno Maria de los
               | Remedios Cipriano de la Santisima Trinidad Ruiz y
               | Picasso_? Really, though, Iterables make more sense for
               | monadic types.
        
               | sdenton4 wrote:
               | We would force broad changes in human society to conform
               | to the assumptions of our database scheme, same as we
               | always have.
        
         | dragonwriter wrote:
         | > Anything after,*, is a kwarg.
         | 
         | A required positional OR kwarg as you've done it. Its closer to
         | an optional kwarg if you expand the type declaration to also
         | allow None and set a None default.
         | 
         | But there are times when you want to leave the number and names
         | of kwargs open (one example is for a dynamic wrapper--a
         | function that wraps another function that can be different
         | across invocations.)
        
         | [deleted]
        
       | hk__2 wrote:
       | > In the function body, args will be a tuple, and kwargs a dict
       | with string keys.
       | 
       | This always bugs me: why is `args` immutable (tuple) but `kwargs`
       | mutable (dict)? In my experience it's much more common to have to
       | extend or modify `kwargs` rather than `args`, but I would find
       | more natural having an immutable dict for `kwargs`.
        
         | adamchainz wrote:
         | Yeah, that is odd. Python still has no immutable dict type,
         | except it kinda does: https://adamj.eu/tech/2022/01/05/how-to-
         | make-immutable-dict-...
        
         | dragonwriter wrote:
         | > This always bugs me: why is `args` immutable (tuple) but
         | `kwargs` mutable (dict)?
         | 
         | Because python didn't (still doesn't, but at this point even if
         | it did backward compatibility would mean it wouldn't be used
         | for this purpose) have a basic immutable mapping type to use.
         | 
         | (Note, yes, MappingProxyType exists, but that's a proxy without
         | mutation operations, not a basic type, so it costs a level of
         | indirection.)
        
       | blibble wrote:
       | now try typing a decorator
       | 
       | https://stackoverflow.com/questions/47060133/python-3-type-h...
       | 
       | what a disaster
        
         | amethyst wrote:
         | PEP 612 made this much better FWIW.
         | 
         | https://peps.python.org/pep-0612/
        
       | akasakahakada wrote:
       | Although these two comes in handly, people have been using them
       | wrong. Often in scientific open source package, they slap *kwargs
       | in function definition without documentation. How am I suppose to
       | know what to pass in?
       | 
       | https://qiskit.org/ecosystem/aer/stubs/qiskit_aer.primitives...
        
         | tomn wrote:
         | OT, but this is my number one peeve with code documentation:
         | going to the effort to write a doc comment, taking up at least
         | 6 lines, probably using some special syntax, cluttering up the
         | code, but then adding no information that can't be derived from
         | the signature.
         | 
         | If you're not going to document something (which I totally
         | respect), at least don't make the code worse while doing it.
        
         | [deleted]
        
         | toxik wrote:
         | Sadly a problem with any wrapper function is that it nullifies
         | this kind of information. Use functools.wraps.
        
           | akasakahakada wrote:
           | My question is that can @warps warp more than 1 function?
           | 
           | Maybe in some use case people need to merge 2 functions into
           | 1, I don't know if it can handle this situation.
        
             | zbentley wrote:
             | I'm not sure what it means to "merge two functions into
             | one", can you elaborate?
             | 
             | If you are referring to a type signature for a function
             | that passes through it's arguments to one of two inner
             | functions, each of which has different signatures, such
             | that the outer signature accepts the union of the two inner
             | signatures, well ... you _could_ achieve that with
             | ParamSpecs or similar, but it would be pretty hard to read
             | and indirected. Better, I 'd say, to manually express
             | appropriate typing.Union (|) annotations on the outer
             | function, even if that is a little less DRY.
        
               | cbarrick wrote:
               | > I'm not sure what it means to "merge two functions into
               | one", can you elaborate?
               | 
               | I'm not OP, but I see this pattern often enough:
               | def foo(**kwargs):             pass              der
               | bar(**kwargs):             pass              def
               | wrapper(**kwargs):             foo(**kwargs)
               | bar(**kwargs)
        
               | akasakahakada wrote:
               | Yup, this exactly.
        
           | franga2000 wrote:
           | PyCharm usually figured this out if it's not too complex. I
           | often wrap session.request() with some defaults/overrides and
           | autocomplete usually shows me the base arguments as well.
        
         | Syntaf wrote:
         | Especially when they don't even leave a doc string so you're
         | forced to track down the packages documentation online just to
         | interact with certain interfaces.
         | 
         | I work in a large python codebase, we have almost no usage of
         | `*kwargs` beyond proxy methods because of the nature of how
         | they obfuscate the real interface for other developers.
        
           | nerdponx wrote:
           | The worst is when someone puts **kwargs at the _base_ of a
           | class hierarchy, not only necessitating its use in subclasses
           | (if you want to be strict about types) but also swallowing
           | errors for no good reason. Fortunately I think this style is
           | fading out as type hints become more popular.
        
           | hqudsi wrote:
           | When I was first starting out, a then senior engineer told
           | me: "friends don't let other friends use kwargs".
           | 
           | That always stuck with me.
        
             | icedchai wrote:
             | I once worked on a code base where we had *kwargs passed
             | down 4 or 5 layers deep (not my idea.) It was a true joy.
        
               | akasakahakada wrote:
               | This is literally me. It is a math program that can
               | evaluate equations and generate code. 6 layers of
               | heterogeneous data structure which the math operation
               | being act on 1st layer has its effect down to 6th layer.
               | Temporarily using *kwargs to make it works but still
               | thinking what is the proper way to do it right.
        
               | crazydoggers wrote:
               | Can you organize the data structures into classes or
               | dataclasses?
        
               | akasakahakada wrote:
               | Already doing this. The problem is there are 5 layers in
               | between. Copy and paste the same docstring into all
               | layers is doable but do not seem smart.
        
               | c32c33429009ed6 wrote:
               | Out of interest, what sort of company/industry do you
               | work in where you're able to work on this kind of thing?
        
         | dr_kiszonka wrote:
         | I have been annoyed by this too! I like how seaborn handles it
         | now in documentation:
         | https://seaborn.pydata.org/generated/seaborn.barplot.html?hi...
        
       | itissid wrote:
       | #TIL. Also cool to know is pydantic's @validate decorator:
       | https://docs.pydantic.dev/latest/usage/validation_decorator/...
       | and in case you were thinking its not superflous to mypy(https://
       | docs.pydantic.dev/latest/usage/validation_decorator/...).
        
       | SushiHippie wrote:
       | For typing **kwargs there are TypedDicts
       | https://peps.python.org/pep-0692/
       | 
       | If your function just wraps another you can use the same type
       | hints as the other function with functools.wraps
       | https://docs.python.org/3/library/functools.html#functools.w...
        
         | awinter-py wrote:
         | I think pep 612 is trying to make the ergonomics better for the
         | 'forwarding' / pass-through case (when .wraps isn't
         | appropriate)
         | 
         | https://peps.python.org/pep-0612/
        
         | zbentley wrote:
         | While functools.wraps does propagate __annotations__ by
         | default, be aware that not all IDE-integrated type checkers
         | handle that properly. It's easy in PyCharm, for example, to use
         | functools.wraps such that the wrapper function is treated by
         | the IDE as untyped.
         | 
         | Underneath, this is because many (most?) type checkers for
         | Python aren't actually running the code in order to access
         | annotation information, and are instead parsing it "from the
         | outside" using complex and fallible techniques of variable
         | reliability. That said, it's a testament to JetBrains'
         | excellent work that PyCharm's checker works as well as it does,
         | given how crazily metaprogrammed even simple Python often turns
         | out to be.
        
           | veber-alex wrote:
           | Pycharm has the worst type checker that exists today. It may
           | have been the best a few years back but others have
           | suppressed it considerably.
           | 
           | I recently switched from Pycharm to vscode which uses pyright
           | and it's night and day on the amount of type errors it
           | catches, it considerably improved the quality of my code and
           | confidence during refactoring.
           | 
           | And to add insult to injury Pycharm doesn't even have a
           | pyright plugin and the mypy plugin is extremely slow and
           | buggy.
        
         | dalf wrote:
         | There is also typing.ParamSpec when the purpose is to write a
         | generic wrapper:
         | 
         | https://docs.python.org/3/library/typing.html#typing.ParamSp...
        
         | ehsankia wrote:
         | Interesting, looks like they ended up having to introduce
         | typing.Unpack, to differentiate the ambiguity with the the
         | TypedDict referring to the type of all the kwargs, vs just
         | Mapping[str, TypedDict]
         | 
         | Not ideal but not too bad either.
        
       | refactor_master wrote:
       | 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.
        
         | liquidpele wrote:
         | Yea, really only useful imho for proxy functions that then just
         | pass the arguments along to something that DOES properly type
         | every arg.
        
           | qwertox wrote:
           | But doesn't this break type checking for the users of the
           | proxy functions?
        
             | flakes wrote:
             | You can write the proxy/decorator to preserve typing info
             | using a typevar.                   F = TypeVar("F",
             | bound=Callable)         def wrapper(f: F) -> F: ...
        
         | zbentley wrote:
         | 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)?
        
           | refactor_master wrote:
           | 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.
        
             | voz_ wrote:
             | As someone that works on a Python compiler, this is a very
             | limited view of reality...
        
           | plonk wrote:
           | Those are better handled by typing.ParamSpec, it should keep
           | track of the unwrapped function's arguments.
        
         | hooloovoo_zoo wrote:
         | Seems pretty important for something like a plotting function
         | where you want to be able to pass any tweaks to any subplots.
        
         | jerpint wrote:
         | What do you do when inheriting from a base class with a defined
         | __init__ ?
        
           | yayachiken wrote:
           | 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.)
        
             | bowsamic wrote:
             | This is why I hate Python, absolutely none of this is
             | obvious from the design of the language
        
             | sbrother wrote:
             | 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.
        
             | Izkata wrote:
             | > 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.
        
             | dontlaugh wrote:
             | Having used Python a lot, I was never glad for multiple
             | inheritance. I'd prefer traits.
        
             | patrickkidger wrote:
             | 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.)
        
         | [deleted]
        
         | roland35 wrote:
         | I agree - it is convenient to use at first but it sure makes it
         | hard to use an unfamiliar codebase!
        
         | codexb wrote:
         | They are a necessity for subclasses though, especially when
         | subclassing from an external library that will likely change
         | underneath you.
        
       ___________________________________________________________________
       (page generated 2023-08-27 23:00 UTC)