Why do you say that duck typing is simplified structural typing?
Its relationship with structural typing is on a different axis.
Duck typing is its dynamic-typing counterpart.
from dataclasses import dataclass
from typing import Protocol
class Globular(Protocol):
diameter: float
class Spherical(Protocol):
diameter: float
# In Python, we need to define concrete classes that implement the protocols.
@dataclass
class Ball:
diameter: float
@dataclass
class Sphere:
diameter: float
ball: Globular = Ball(diameter=10)
sphere: Spherical = Sphere(diameter=20)
# These assignments work because both types structurally conform to the protocols.
sphere = ball
ball = sphere
class Tubular(Protocol):
diameter: float
length: float
@dataclass
class Tube:
diameter: float
length: float
tube: Tubular = Tube(diameter=12, length=3)
tube = ball # Fail type check.
ball = tube # Passes.
This is what Pyright says about it:
Found 1 error.
/scratch/structural.py
/scratch/structural.py:37:8 - error: Type "Ball" is not assignable to declared type "Tubular"
"Ball" is incompatible with protocol "Tubular"
"length" is not present (reportAssignmentType)
1 error, 0 warnings, 0 informations
Edit: And this is ty:
error: lint:invalid-assignment: Object of type `Ball` is not assignable to `Tubular`
--> structural.py:37:1
|
35 | tube: Tubular = Tube(diameter=12, length=3)
36 |
37 | tube = ball # Fail type check.
| ^^^^
38 | ball = tube # Passes.
|
info: `lint:invalid-assignment` is enabled by default
Found 1 diagnostic
I'd go a step further and say that duck typing is more than just structural typing's dynamic counterpart. Because, again, that's confounding two different axes. Dynamic vs static describes when type checking happens and whether types are associated with names or with values. But it doesn't necessarily describe the definition of "type".
The real difference between structural typing and duck typing is that structural typing requires all of a type's declared members to be present for an object to be considered compatible. Duck typing only requires the members that are actually being accessed to be present.
This is definitely more common in dynamic languages, but I'm not aware of any particular reason why that kind of checking couldn't also be done statically.
If I understand correctly, defining the protocol like this forces the implementation classes to have the members as proper fields and disallows properties. If you define `diameter` as a property in the protocol, it supports both:
from dataclasses import dataclass
from typing import Protocol
class Field(Protocol):
diameter: float
class Property(Protocol):
@property
def diameter(self) -> float: ...
class Ball:
@property
def diameter(self) -> float:
return 1
@dataclass
class Sphere:
diameter: float
ball_field: Field = Ball()
sphere_field: Field = Sphere(diameter=20)
ball_prop: Property = Ball()
sphere_prop: Property = Sphere(diameter=20)
Pyright output:
/Users/italo/dev/paper-hypergraph/t.py
/Users/italo/dev/paper-hypergraph/t.py:27:21 - error: Type "Ball" is not assignable to declared type "Field"
"Ball" is incompatible with protocol "Field"
"diameter" is invariant because it is mutable
"diameter" is an incompatible type
"property" is not assignable to "float" (reportAssignmentType)
1 error, 0 warnings, 0 information
That is to say, I find Python's support for structural typing to be limited in practice.
Python does support structural typing through protocols introduced in version 3.8. They are documented in https://typing.python.org/en/latest/spec/protocol.html.
As a demo, here is part of https://www.typescriptlang.org/play/typescript/language/stru... translated to Python:
This is what Pyright says about it: Edit: And this is ty: