Two ways to call
Distinct call signatures for the same function are defined using typing.overload
.
Explicit parameters
To force a function parameter to be "keyword-only", you can insert a bare *
in the parameter list before it.
Sensible defaults
Since you'll be likely dealing with float
s only, a convenient object is the math.nan
singleton. It is a float
instance, but always non-equal to any other float
. This allows you to keep your parameter type constrained to float
as opposed to float | None
for example.
Exclusive or
The int
class (of which bool
is a subclass) has the bitwise exclusive or operator ^
defined for it. By combining that with math.isnan
we can therefore concisely check that only exactly one of the two arguments were provided.
Suggested implementation
Full working example:
from math import isnan, nan
from typing import overload
class Circle:
@overload
def __init__(self, *, radius: float) -> None:
...
@overload
def __init__(self, *, diameter: float) -> None:
...
def __init__(self, *, radius: float = nan, diameter: float = nan) -> None:
"""Takes either a `radius` or a `diameter` but not both."""
if not isnan(radius) ^ isnan(diameter):
raise TypeError("Either radius or diameter required")
self.radius = radius if isnan(diameter) else diameter / 2
if __name__ == "__main__":
c1 = Circle(radius=1)
c2 = Circle(diameter=2)
assert c1.radius == c2.radius
# Circle(radius=3.14, diameter=42) # error
# Circle() # same error
Some things to note
If you try this with for example PyCharm, after typing Circle
and an opening parenthesis you'll see in a little popover the two possible calls listed in the order they were defined to hint to you that you have these two distinct options for calling the function. It does not show you the actual implementation's signature, where you have both parameters present.
If you add reveal_type(Circle)
at the bottom and run mypy
over that module, you'll get the following:
note: Revealed type is "Overload(def (*, radius: builtins.float) -> Circle, def (*, diameter: builtins.float) -> Circle)"
I agree with @dskrypa regarding names. See PEP 8 for more.
Also, the reason I defined a TypeError
here is that this exception class is used by Python, when a function is called with unexpected arguments or arguments missing.
Finally, the ternary x if expr else y
-construct is warranted, when you are dealing with a very simple expression and have two mutually exclusive and very simple assignment options. This is the case here after our check, so we can use it and make the code much shorter, as well as (arguably) cleaner and easier to read.
PS: In case you are wondering, bitwise XOR takes precedence over not
, which is why not a ^ b
without parantheses is effectively a
XNOR b
.