I am not aware of any possible way to achieve this in Python as you cannot provide such constraints in type hints.


However, probably the Collection type might be useful in your context as a workaround:

class collections.abc.Collection

ABC for sized iterable container classes.

This requires objects to have a __len__, which is a more strict requirement than being finite. For example, finite generators don't count as Collection.

Answer from Giorgos Myrianthous on Stack Overflow
🌐
Python.org
discuss.python.org › ideas
Quicker way of type hinting Iterable - Ideas - Discussions on Python.org
May 3, 2023 - Many functions expect parameters that are iterable and do not care if they are actually lists. But, instead of importing typing.Iterable, one may be tempted to just use list[type] and call it a day. Like list can be made generic in a type hint, why not allow me to use iter like that as well?
Discussions

How to Implement Positional Type Hinting for Iterables in Python?
]I’ve recently written a piece of code that looks something like this: def display_info(input_data: tuple[int, str, dict]): numeric_value, text, key_value_store = input_data print(numeric_value, text, key_value_store) However, I’ve noticed that this type hinting approach might not be the ... More on discuss.python.org
🌐 discuss.python.org
2
1
September 3, 2023
Python type hint for Iterable[str] that isn't str - Stack Overflow
In Python, is there a way to distinguish between strings and other iterables of strings? A str is valid as an Iterable[str] type, but that may not be the correct input for a function. For example, in More on stackoverflow.com
🌐 stackoverflow.com
Type hinting a non-iterator (non-consumable) iterable
It would be great if there was a type that allows any Iterable that cannot be consumed, like a list, but not the iterator of a list. When searching, the only thing I found was an unanswered SO question. ... topic: featureDiscussions about new features for Python's type annotationsDiscussions ... More on github.com
🌐 github.com
10
December 24, 2022
Type hints for Sized Iterable in Python - Stack Overflow
I have a function that uses the len function on one of it's parameters and iterates over the parameter. Now I can choose whether to annotate the type with Iterable or with Sized, but both gives err... More on stackoverflow.com
🌐 stackoverflow.com
🌐
Python documentation
docs.python.org › 3 › library › typing.html
typing — Support for type hints
The problem with this approach is that a class had to be explicitly marked to support them, which is unpythonic and unlike what one would normally do in idiomatic dynamically typed Python code. For example, this conforms to PEP 484: from collections.abc import Sized, Iterable, Iterator class Bucket(Sized, Iterable[int]): ...
🌐
Python.org
discuss.python.org › python help
How to Implement Positional Type Hinting for Iterables in Python? - Python Help - Discussions on Python.org
September 3, 2023 - ]I’ve recently written a piece of code that looks something like this: def display_info(input_data: tuple[int, str, dict]): numeric_value, text, key_value_store = input_data print(numeric_value, text, key_value_store) However, I’ve noticed that this type hinting approach might not be the most flexible.
🌐
Daily Dose of DS
blog.dailydoseofds.com › daily dose of data science › 10 ways to declare type hints in python
10 Ways to Declare Type Hints in Python - by Avi Chawla
March 26, 2024 - For list, tuple and dict specifically, ... Declare such objects as follows: Iterables are objects you can iterate on — list, tuple, or dict....
Find elsewhere
🌐
Cpske
cpske.github.io › ISP › type-hints › introduction
Type Hints –– An Introduction | Individual Software Process
All the standard type extensions: List, Tuple, Set, etc. Iterable when I only require a function / method’s input to be an iterable (that is, I can use a for loop with it at least once) Callable when you’re making higher level functions or using a function somewhere ... See the typing module for more details. PEP 526 introduces a new syntax for defining variables available from Python 3.6 onwards.
🌐
GitHub
github.com › python › typing › issues › 1319
Type hinting a non-iterator (non-consumable) iterable · Issue #1319 · python/typing
December 24, 2022 - I want to annotate a function taking an iterable, that is used multiple times. For example: def func(thing: Iterable[str]) -> None: for _ in range(10): for x in thing: do_thing(x) Iterator is a valid Iterable, but passing an Iterator to ...
Author: python
🌐
Pybites
pybit.es › articles › code-better-with-type-hints-part-2
Code Better with Type Hints – Part 2 – Pybites
August 27, 2021 - You have to explicitly require an iterable which elements are of type numbers. If you do not specify the type of the elements (or any variable, for that matter), their type hint becomes Any by default, or Unknown in the case of Pylance, which ...
🌐
Mypy
mypy.readthedocs.io › en › stable › cheat_sheet_py3.html
Type hints cheat sheet - mypy 2.3.1 documentation
In typical Python code, many functions that can take a list or a dict as an argument only need their argument to be somehow “list-like” or “dict-like”. A specific meaning of “list-like” or “dict-like” (or something-else-like) is called a “duck type”, and several duck types that are common in idiomatic Python are standardized. from collections.abc import Mapping, MutableMapping, Sequence, Iterable # or 'from typing import ...' (required in Python 3.8) # Use Iterable for generic iterables (anything usable in "for"), # and Sequence where a sequence (supporting "len" and "__get
🌐
Iifx
iifx.dev › en › articles › 457230761 › iterable-vs-iterator-type-hinting-for-reusable-data-in-python
python typing - Iterable vs. Iterator: Type Hinting for Reusable Data in Python
The type hint you should use for an object that can be iterated through multiple times is typing.Iterable (or simply Iterable if you're using Python 3.9+ or imported from typing).
🌐
Stack Overflow
stackoverflow.com › questions › 71831067 › python-type-hint-for-any-class-which-is-iterable-with-fixed-number-of-elements
Python: Type hint for any class which is iterable with fixed number of elements - Stack Overflow
I have a function that has an argument which can be any object that can be iterated on and return two ints, such as Tuple[int, int] or List[int, int]. More so, any custom class which has __getitem__() or __iter__() methods that allow accessing and unpacking two ints should also work, such that: from custom_class import CustomClass c = CustomClass() # c[0] = 10 # c[1] = 20 # c[i] -> Error , for i != 0,1 def my_func(item: ?) -> Tuple[int, int]: i, j = item # ... return tuple(i, j) Which should be the type hint used for the item argument in this case?
Top answer
1 of 3
107

The Sequence and Iterable abstract base classes (can also be used as type annotations) mostly* follow Python's definition of sequence and iterable. To be specific:

  • Iterable is any object that defines __iter__ or __getitem__.
  • Sequence is any object that defines __getitem__ and __len__. By definition, any sequence is an iterable. The Sequence class also defines other methods such as __contains__, __reversed__ that calls the two required methods.

Some examples:

  • list, tuple, str are the most common sequences.
  • Some built-in iterables are not sequences. For example, reversed returns a reversed object (or list_reverseiterator for lists) that cannot be subscripted.

* Iterable does not exactly conform to Python's definition of iterables — it only checks if the object defines __iter__, and does not work for objects that's only iterable via __getitem__ (see this table for details). The gold standard of checking if an object is iterable is using the iter builtin.

2 of 3
23

When writing a function/method with an items argument, I often prefer Iterable to Sequence. Hereafter is why and I hope it will help understanding the difference.

Say my_func_1 is:

from typing import Iterable
def my_func_1(items: Iterable[int]) -> None:
    for item in items:
        ...
        if condition:
            break
    return

Iterable offers the maximum possibilities to the caller. Correct calls include:

my_func_1((1, 2, 3)) # tuple is Sequence, Collection, Iterator
my_func_1([1, 2, 3]) # list is MutableSequence, Sequence, Collection, Iterator
my_func_1({1, 2, 3}) # set is Collection, Iterator
my_func_1(my_dict) # dict is Mapping, Collection, Iterator
my_func_1(my_dict.keys()) # dict.keys() is MappingKeys, Set, Collection, Iterator
my_func_1(range(10)) # range is Sequence, Collection, Iterator
my_func_1(x**2 for x in range(100)) # "strict' Iterator, i.e. neither a Collection nor a Sequence
... 

... because all areIterable.

The implicit message to a function caller is: transfer data "as-is", just don't transform it.

In case the caller doesn't have data as a Sequence (e.g. tuple, list) or as a non-Sequence Collection (e.g. set), and because the iteration breaks before StopIteration, it is also more performing if he provides an 'strict' Iterator.

However if the function algorithm (say my_func_2) requires more than one iteration, then Iterable will fail if the caller provides a 'strict' Iterator because the first iteration exhausts it. Hence use a Collection:

from typing import Collection
def my_func_2(items: Collection[int]) -> None:
    for item in items:
        ...
    for item in items:
        ...
    return

If the function algorithm (my_func_3) has to access by index to specific items, then both Iterable and Collection will fail if the caller provides a set, a Mapping or a 'strict' Iterator. Hence use a Sequence:

from typing import Sequence
def my_func_3(items: Sequence[int]) -> None:
    return items[5]

Conclusion: The strategy is: "use the most generic type that the function can handle". Don't forget that all this is only about typing, to help a static type checker to report incorrect calls (e.g. using a set when a Sequence is required). Then it's the caller responsibility to transform data when necessary, such as:

my_func_3(tuple(x**2 for x in range(100)))

Actually, all this is really about performance when scaling the length of items. Always prefer Iterator when possible. Performance shall be handle as a daily task, not as a firemen task force.

In that direction, you will probably face the situation when a function only handles the empty use case and delegates the others, and you don't want to transform items into a Collection or a Sequence. Then do something like this:

from more_itertools import spy
def my_func_4(items: Iterable[int]) -> None:
    (first, items) = spy(items)
    if not first: # i.e. items is empty
        ...
    else:
        my_func_1(items) # Here 'items' is always a 'strict' Iterator
    return
🌐
Stack Overflow
stackoverflow.com › questions › 79769321 › what-to-put-as-python-type-hint-when-any-kind-of-iterable-works
function - What to put as Python type hint when any kind of iterable works? - Stack Overflow
My function can input an argument in the form of any iterable like list, tuple, etc. as long as it is indexable. What should I put as the type in the type hint? I tried writing def foo(items: list or tuple), but VS Code said "Binary operator not allowed in type expression." FYI I am using Python 3.11.9.
🌐
Python Forum
python-forum.io › thread-16557.html
What is the correct type hint when you want to accept Iterable but not Dictionary
How would I indicate, using a type hint, that the function only works with a List type data structure containing only int? I researched a little deeper and from the documentation, I found: Quote:An object capable of returning its members one at a...
Top answer
1 of 1
3

Running mypy in --strict mode actually tells you everything you need.

1) Incomplete Iterable

:13: error: Missing type parameters for generic type "Iterable"  [type-arg]

Since Iterable is generic and parameterized with one type variable, you should subclass it accordingly, i.e.

...
T = typing.TypeVar("T", bound="Element")
...
class BaseIterableClass(
    abc.ABC,
    collections.abc.Iterable[T],
    SomeClassIHaveToDeriveFrom,
):

2) Now we get a new error

:17: error: Return type "Iterator[Element]" of "__iter__" incompatible with return type "Iterator[T]" in supertype "Iterable"  [override]

Easily solvable:

...
    @abc.abstractmethod
    def __iter__(self) -> typing.Iterator[T]:

3) Now that we made BaseIterableClass properly generic...

:20: error: Missing type parameters for generic type "BaseIterableClass"  [type-arg]

Here we can specify Element:

class A(BaseIterableClass[Element]):
...

4) Missing return types

:21: error: Function is missing a type annotation  [no-untyped-def]
:24: error: Function is missing a return type annotation  [no-untyped-def]

Since we are defining the methods __iter__ and __next__ for A, we need to annotate them properly:

...
    def __iter__(self) -> collections.abc.Iterator[Element]:
...
    def __next__(self) -> Element:

5) Wrong return value

Now that we annotated the __next__ return type, mypy picks up that "some string that isn't an Element" is not, in fact, an instance of Element. 🙂

:25: error: Incompatible return value type (got "str", expected "Element")  [return-value]

Fully annotated code

from abc import ABC, abstractmethod
from collections.abc import Iterable, Iterator
from typing import TypeVar


T = TypeVar("T", bound="Element")


class Element:
    pass


class SomeClassIHaveToDeriveFrom:
    pass


class BaseIterableClass(
    ABC,
    Iterable[T],
    SomeClassIHaveToDeriveFrom,
):
    @abstractmethod
    def __iter__(self) -> Iterator[T]:
        ...


class A(BaseIterableClass[Element]):
    def __iter__(self) -> Iterator[Element]:
        return self

    def __next__(self) -> Element:
        return "some string that isn't an Element"  # error
        # return Element()

Fixed type argument

If you don't want BaseIterableClass to be generic, you can change steps 1)-3) and specify the type argument for all subclasses. Then you don't need to pass a type argument for A. The code would then look like so:

from abc import ABC, abstractmethod
from collections.abc import Iterable, Iterator


class Element:
    pass


class SomeClassIHaveToDeriveFrom:
    pass


class BaseIterableClass(
    ABC,
    Iterable[Element],
    SomeClassIHaveToDeriveFrom,
):
    @abstractmethod
    def __iter__(self) -> Iterator[Element]:
        ...


class A(BaseIterableClass):
    def __iter__(self) -> Iterator[Element]:
        return self

    def __next__(self) -> Element:
        return "some string that isn't an Element"  # error
        # return Element()

Maybe Iterator instead?

Finally, it seems that you actually want the Iterator interface, since you are defining the __next__ method on your subclass A. In that case, you don't need to define __iter__ at all. Iterator inherits from Iterable and automatically gets __iter__ mixed in, when you inherit from it and implement __next__. (see docs)

Also, since the Iterator base class is abstract already, you don't need to include __next__ as an abstract method.

Then the (generic version of the) code would look like this:

from abc import ABC
from collections.abc import Iterator
from typing import TypeVar


T = TypeVar("T", bound="Element")


class Element:
    pass


class SomeClassIHaveToDeriveFrom:
    pass


class BaseIteratorClass(
    ABC,
    Iterator[T],
    SomeClassIHaveToDeriveFrom,
):
    pass


class A(BaseIteratorClass[Element]):
    def __next__(self) -> Element:
        return "some string that isn't an Element"  # error
        # return Element()

Both iter(A()) and next(A()) work.

Hope this helps.