How to type a Tuple with many elements in Python?
Asked Answered
L

3

7

I am experimenting with the typing module and I wanted to know how to properly type something like a Nonagon (a 9 point polygon), which should be a Tuple and not a List because it should be immutable.

In 2D space, it would be something like this:

Point2D = Tuple[float, float]
Nonagon = Tuple[Point2D, Point2D, Point2D, Point2D, Point2D, Point2D, Point2D, Point2D, Point2D]

nine_points: Nonagon = (
    (0.0, 0.0),
    (6.0, 0.0),
    (6.0, 2.0),
    (2.0, 2.0),
    (6.0, 5.0),
    (2.0, 8.0),
    (6.0, 8.0),
    (6.0, 10.0),
    (0.0, 10.0),
)

Is there any syntactic sugar available to make the Nonagon declaration shorter or easier to read?

This is not valid Python, but I am looking for something similar to this:

Nonagon = Tuple[*([Point2D] * 9)]  # Not valid Python

Or using NamedTuple

# Not properly detected by static type analysers
Nonagon = NamedTuple('Nonagon', [(f"point_{i}", Point2D) for i in range(9)])  

This is NOT what I want:

# Valid but allows for more and less than 9 points
Nonagon = Tuple[Point2D, ...]  

I think the most adequate way would be something like:

from typing import Annotated

# Valid but needs MinMaxLen and checking logic to be defined from scratch
Nonagon = Annotated[Point2D, MinMaxLen(9, 9)]  
Lepore answered 24/4, 2022 at 23:46 Comment(5)
Are you doing this inside of a class, or just at the module level? Also which python version is this for?Continually
My question is more on a conceptual level so it could be either on a module level or inside a class. No Python version requirement, so I would accept an answer even if targets 3.11 or is PEP that covers this type of syntaxBedrabble
You should respect geometric objects, and give them a proper type; maybe a subclass of tuple?Barnacle
@ReblochonMasque But what if we need to define a 100 points polygon for that matter? Having a class with 100 attributes is not something readable at all. And the question just uses geometry objects as example, this applies to constant lenght Tuples more specificallyBedrabble
A tuple is a product of types, so what you need is type-level integers and type-level "exponentiation" to define the product of Point3D with itself 9 times. Practically speaking, the fact that Nonagon is a type product isn't really important, and you would just define a class Nonagon whose constructor happens to take 9 arguments to initialize a value.Encincture
C
4

You can use the types module. All type hints come from types.GenericAlias.

From the doc:

Represent a PEP 585 generic type
E.g. for t = list[int], t.__origin__ is list and t.__args__ is (int,).

This means that you can make your own type hinting by passing the type arguments to the class itself.

>>> Point2D = tuple[float, float]
>>> Nonagon = types.GenericAlias(tuple, (Point2D,)*9)
>>> Nonagon
tuple[tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float]]
Continually answered 25/4, 2022 at 0:4 Comment(5)
This looks exactly like what I am looking for, is there a way to avoid complains from mypy when doing this? I get "Variable "mymodule.Nonagon" is not valid as a type" if using TypeAlias I get "Invalid type alias: expression is not a valid type"Bedrabble
Why python_typing (are you sure you imported types)? It could also be the checker only allowing you to do it with literals, since this construct isn't that common. However, the syntax is correct. For example try types.GenericAlias(tuple, (int, int)) == tuple[int, int], which returns True.Continually
mypy would have to be (and doesn't appear to have been yet) written to recognize what GenericAlias does. Defining the type at run-time is too late for mypy to use it.Encincture
python_typing is the filename, I changed to mymodule to avoid confusion. Then if it isn't properly detected that should be an issue on mypy right? I follow your logic and I agree it should've been detected properlyBedrabble
When I tried this on pycharm, it didn't recognize that this was a type alias. So yes, technically this is the correct syntax. I'm pretty sure that no code checker is able to do this because GenericAlias is made during runtime. (If there is one please tell me!)Continually
K
3

The very first way you described it:

Nonagon = Tuple[Point2D, Point2D, Point2D, Point2D, Point2D, Point2D, Point2D, Point2D, Point2D]

is the right way to do it, as discussed in a 2016 python/typing issue. Yes, it's a bit ugly, but you only have to define it once.

You could leverage Annotated here, but it ultimately depends on whether your typechecker can make use of the annotations that you include. Not all typecheckers would necessarily make use of those annotations, whereas spelling out how many elements should be in the tuple is something that every (functioning) typechecker should recognize.

Kreis answered 25/4, 2022 at 0:3 Comment(0)
A
-3

Not sure if this would work for your purposes, but numpy might be useful here.


import numpy as np
ones = np.ones(9)
nonogon = ones * Point2D

Atypical answered 24/4, 2022 at 23:53 Comment(2)
This doesn't work - TypeError: unsupported operand type(s) for *: 'float' and 'types.GenericAlias'. You can't multiply 1.0 by tuple[float, float], which is a type hint.Continually
@Atypical I am looking for a pure Python solution, something that can be achieved with the standard library onlyBedrabble

© 2022 - 2024 — McMap. All rights reserved.