Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Immutable Sequence of float with length 3 as argument #2163

RubendeBruin started this conversation in General
Discussion options

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).

You must be logged in to vote

Replies: 1 comment

Comment options

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.

  1. 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.

  1. 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.

  1. 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).

  1. 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"

  1. 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

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /