-
Notifications
You must be signed in to change notification settings - Fork 288
Immutable Sequence of float with length 3 as argument #2163
-
I need some guidance on implementing type-hinting for 3d vectors.
We are trying to add type-hits to our 3d vector math functions. Many of these functions require a sequence of 3 floats as input. For example:
def to_global(point : tuple[float, float, float]) -> ....:
tuple[float, float, float] defines all that we need: The input argument shall have three indexable elements point[0], point[1], point[2] and their values shall be float.
Issue is that while duck-type compatible is also fine as input: so [1,2,3] (list of int) would also work without issues:
b = to_global([1,2,3])
but in those case the type checker does raise a warning about incompatible types.
From the best practices I read that in cases like this a Generic type would be preferable:
def to_global(point : Sequence[float]) -> ....:
but that does not cover the length.
If I now try:
b = to_global([1,2])
the type checker no longer warns, but the code does not run as point[2] is not available.
Is there a way to get the best of both? Ie the strictness of tuple[float, float, float] but without the type-checker complaining when trying to pass a duck compatible input argument?
Bonus:
I would also like to hint that the values of the input argument are not mutated, (like const in c).
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 1 comment
-
Hello, What you’re running into is a real limitation (and design trade-off) of Python’s static typing: shape (length) and element type are only strictly enforceable for tuples, not for general sequences.
That said, you can get very close to "the best of both worlds" with a few established patterns.
- Why tuple[float, float, float] is strict (and complains)
def to_global(point: tuple[float, float, float]) -> ...
This is the most precise type:
Exactly 3 elements
Each element is float
Immutable
The warning you see when calling:
to_global([1, 2, 3])
is correct behavior from the type checker:
list[int] is not a tuple[float, float, float]
Type checkers intentionally do not assume implicit conversion (int → float) or structural compatibility here
At runtime it works, but static typing is doing its job.
- Why Sequence[float] is too loose
def to_global(point: Sequence[float]) -> ...
This expresses:
"Indexable"
"Elements are floats"
❌ No length guarantee
So [1, 2] is valid statically, even though it explodes at runtime.
Python’s type system currently cannot express "a sequence of length exactly 3" — except with tuples.
- Recommended solution: accept broad input, normalize internally
The most Pythonic and type-safe approach is:
Accept any reasonable sequence of numbers
Convert once at the boundary
Work internally with a strict type
from collections.abc import Sequence
Vector3 = tuple[float, float, float]
def to_global(point: Sequence[float]) -> Vector3:
if len(point) != 3:
raise ValueError("Expected a sequence of length 3")
x, y, z = map(float, point)
return (x, y, z)
Benefits:
✅ Accepts list[int], tuple[int, int, int], numpy arrays, etc.
✅ Enforces length at runtime
✅ Converts to a known, immutable internal representation
✅ Type checker is happy
✅ Callers get a precise return type
This is the pattern used by many numeric libraries (including parts of NumPy, Pydantic, and scientific Python code).
- If you want static length checking: overloads
If you really want to help type checkers catch mistakes at call sites, you can use overloads:
from typing import overload, Sequence
Vector3 = tuple[float, float, float]
@overload
def to_global(point: tuple[float, float, float]) -> Vector3: ...
@overload
def to_global(point: Sequence[float]) -> Vector3: ...
def to_global(point: Sequence[float]) -> Vector3:
if len(point) != 3:
raise ValueError("Expected length 3")
return tuple(map(float, point)) # type: ignore[return-value]
This tells the type checker:
"Tuples of exactly 3 floats are ideal"
"Other sequences are allowed but checked at runtime"
- Bonus: expressing "not mutated" (const-like behavior)
Python typing has no general const keyword, but you already get immutability guarantees via types:
Best options:
Use tuple (immutable by design)
Accept Sequence[T], not MutableSequence[T]
def to_global(point: Sequence[float]) -> Vector3:
...
This explicitly communicates:
"I will read from this, not mutate it."
Static type checkers will prevent accidental mutation like:
point[0] = 1.0 # ❌ error: Sequence is read-only
Beta Was this translation helpful? Give feedback.