How do you build custom data types in Python (similar to how you define interface
in TypeScript)?
Let's say for example I have a class which accepts two input arguments. Both are optional, but one of them is required to initialize the class.
I can do something like:
@dataclass
class Interface:
a: Optional[str] = None
b: Optional[int] = None
def __post_init(self):
if self.a is None and self.b is None:
raise ValueError('one of a or b is required')
Now I want to define a function which accepts an argument of the type Interface
:
def func(i: Interface) -> Interface:
return i
Of course, this works if I invoke:
func(Interface('abc')) # works
func(Interface(2)) # this does not work!
Is it possible to call the function similar to how you would initialize the Interface
class? E.g.:
func('abc') # should work
func(2) # should also work
func([1, 2]) # should report a type issue
The reason for doing this is that I have to make extensive use of the Interface
"type", and would like to avoid writing the types everywhere.
Your current definition still requires the arguments to match properly with the generated __init__
, which has a form like:
def __init__(self, a: Optional[str] = None, b: Optional[int] = None):
...
When you do func('abc')
it works because it's like invoking __init__
with 'abc'
for a
and None
for b
, so the types match. When you do func(2)
, it's still trying to pass 2
to a
, not b
, so the types don't match and you get a (correct) error.
As currently, you must replace func(2)
with func(b=2)
or func(None, 2)
.
If you want to do it with a single positional argument that can be either, you either:
Have to store both in the same field, e.g.:
@dataclass
class Interface:
a: Optional[Union[str, int]] = None # On modern Python, a: str | int | None = None would work
# No postinit needed
with the downside being that now you don't know which is stored there, and have to check each time, or...
Manually write __init__
instead of relying on dataclasses
to generate it, so it accepts a single value and stores it in the correct field:
@dataclass
class Interface:
a: Optional[str] = None
b: Optional[int] = None
def __init__(self, ab: Optional[Union[str, int]] = None):
# Or cleaner 3.10+ syntax:
def __init__(self, ab: str | int | None = None):
self.a = self.b = None
if isinstance(ab, str):
self.a = ab
elif isinstance(ab, int):
self.b = ab
else:
raise TypeError(f"Only str or int accepted, received {type(ab).__name__}")
# No postinit needed