Python
Type Annotations
For Loop
Programming
Code Examples

How do I annotate types in a for-loop?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Python does not support type annotations directly on for loop variables in the loop header. Instead, you declare the type on a separate line before the loop, or rely on type inference from the iterable's type hint. Type checkers like mypy infer the loop variable type automatically when the iterable is properly annotated, so explicit annotation is rarely needed.

The Problem

python
# This is NOT valid Python syntax
for x: int in range(10):  # SyntaxError
    print(x)

Python's for statement does not accept inline type annotations on the loop variable. The grammar simply does not support it.

Method 1: Annotate Before the Loop

python
x: int
for x in range(10):
    print(x)

The x: int declaration tells type checkers that x is an int. The for loop then assigns to x without needing its own annotation. This is the officially supported approach.

Method 2: Let Type Inference Work

When the iterable is properly typed, type checkers infer the loop variable automatically:

python
1numbers: list[int] = [1, 2, 3, 4, 5]
2for num in numbers:
3    # mypy knows num is int — no annotation needed
4    print(num + 1)
5
6names: list[str] = ["Alice", "Bob"]
7for name in names:
8    # mypy knows name is str
9    print(name.upper())

This is the most common approach. If the iterable has a proper type, the loop variable type is inferred.

Method 3: Type Hint the Function Returning the Iterable

python
1from typing import Iterator
2
3def get_numbers() -> Iterator[int]:
4    yield 1
5    yield 2
6    yield 3
7
8for num in get_numbers():
9    # mypy infers num as int from the return type
10    print(num * 2)

Annotating with Tuple Unpacking

python
1from typing import NamedTuple
2
3class Point(NamedTuple):
4    x: float
5    y: float
6
7points: list[Point] = [Point(1.0, 2.0), Point(3.0, 4.0)]
8
9for point in points:
10    # point is inferred as Point
11    print(point.x, point.y)
12
13# Unpacking in the loop
14p: Point
15for p in points:
16    print(p.x)
17
18# Tuple unpacking
19pair: tuple[str, int]
20for pair in [("Alice", 30), ("Bob", 25)]:
21    name, age = pair
22    print(name, age)

Annotating with enumerate and zip

python
1# enumerate
2items: list[str] = ["a", "b", "c"]
3i: int
4item: str
5for i, item in enumerate(items):
6    print(i, item)
7
8# zip
9names: list[str] = ["Alice", "Bob"]
10ages: list[int] = [30, 25]
11name: str
12age: int
13for name, age in zip(names, ages):
14    print(name, age)

In practice, mypy infers these correctly without the pre-declarations if the input lists are typed.

Annotating Dict Iteration

python
1scores: dict[str, int] = {"Alice": 95, "Bob": 87}
2
3# Keys only
4for name in scores:
5    # name is inferred as str
6    print(name)
7
8# Key-value pairs
9for name, score in scores.items():
10    # name: str, score: int — both inferred
11    print(f"{name}: {score}")
12
13# If you want explicit annotation:
14key: str
15value: int
16for key, value in scores.items():
17    print(key, value)

Using cast() for Complex Cases

When the iterable's type is too broad:

python
1from typing import cast, Any
2
3data: list[Any] = get_external_data()
4
5for item in data:
6    # item is Any — too broad
7    user = cast(dict[str, str], item)
8    print(user["name"])

cast() does not change runtime behavior — it only tells the type checker to treat the value as the specified type.

Custom Iterables

python
1from typing import Iterator
2
3class NumberRange:
4    def __init__(self, start: int, end: int) -> None:
5        self.start = start
6        self.end = end
7
8    def __iter__(self) -> Iterator[int]:
9        current = self.start
10        while current < self.end:
11            yield current
12            current += 1
13
14for num in NumberRange(0, 5):
15    # mypy infers num as int from __iter__ return type
16    print(num)

The return type of __iter__ determines the loop variable type. This is the standard pattern for custom iterables.

Type Checking with mypy

bash
1# Check types
2mypy my_script.py
3
4# Common mypy output for loop type issues:
5# error: Incompatible types in assignment (expression has type "str", variable has type "int")
python
1# This triggers a mypy error:
2x: int
3for x in ["a", "b", "c"]:  # error: str is not int
4    print(x)

Common Pitfalls

  • Inline annotation syntax: for x: int in range(10) is a SyntaxError. Python does not support type annotations in the for header. Annotate on a separate line before the loop.
  • Over-annotating: If your iterable is properly typed, the loop variable type is inferred automatically. Adding redundant annotations clutters the code. Only annotate when the type is ambiguous or when you want to constrain it.
  • Scope of loop variables: In Python, the loop variable persists after the loop ends. The pre-loop x: int annotation applies both inside and after the loop.
  • Using assert isinstance() instead of annotations: assert isinstance(item, int) narrows the type at runtime (and for type checkers), but it adds runtime overhead. Use type annotations for static checking and isinstance only when you need runtime validation.
  • Ignoring Any types: Libraries without type stubs return Any. Loop variables from Any iterables are Any. Use cast() or type: ignore comments when you know the actual type.

Summary

  • Python does not support for x: int in ... syntax — annotate on a separate line: x: int
  • Type checkers infer loop variable types from the iterable's type annotation automatically
  • Annotate the iterable (function return type, variable type) rather than the loop variable for cleaner code
  • Use cast() when the inferred type is too broad (e.g., Any from untyped libraries)
  • Pre-loop annotations are only needed when inference fails or you want to explicitly constrain the type

Course illustration
Course illustration

All Rights Reserved.