Several examples are presented to clarify the definition and application of type annotations in Python.
__future__ syntax
__future__ is a special Python module that lets us opt into language features early—before they become the default behavior. We enable a feature per file with:
1 | from __future__ import feature_name |
Quick facts:
- Purpose: forward-compatibility; smooth migration to new syntax/semantics.
- Scope: only the current module (file).
- Placement: must be the first non-comment line.
- Validation: only known feature names are allowed.
Common modern example:
1 | from __future__ import annotations |
Note: In Python 3.11+, annotations behavior is already the default, so that import is optional.
type annotations
We will give several examples to illustrate it.
from __future__...
from __future__ import annotations
What it does: postpones evaluation of type annotations. Instead of evaluating them immediately, Python stores them as strings.
Why that’s useful:
Forward references without quotes:
1
2
3
4
5from __future__ import annotations
class Node:
def __init__(self, next: Node | None = None) -> None: # no quotes needed
self.next = next(Without the future import on Python <3.11 we’d need
'Node'.)
Notes
the | means “or” (a union type).
A | B = value can be type A or type B. It’s the modern (Python 3.10+) shorthand for typing.Union[A, B].
Examples:
1 | def parse(x: int | str) -> int: # x can be int OR str |
What Node | None = None means?
Inside a class like a linked-list node:
1 | from __future__ import annotations # not needed on Python 3.11+ |
Node | None→ the parameternextmay be either aNodeorNone(i.e., it’s optional / nullable).= None→ if we don’t pass a value, the default isNone.- So the constructor allows
Node(), orNode(next=another_node).
Outside of type hints, | is the bitwise OR operator (for ints) and also used for set/dict union. In annotations it’s specially defined to build union types.
- Fewer import cycles: we can annotate with types defined later or in modules that would otherwise create circular imports.
- Faster startup: heavy typing objects aren’t constructed at import time. If we do need concrete types at runtime, use
typing.get_type_hints()which evaluates them on demand.
Version note: In Python 3.11+, postponed evaluation is the default; the import is harmless but unnecessary. In 3.7–3.10, it’s very helpful.
Location: Must be the first non-comment line in the file.
def __init__...
def __init__(self, arg1: str, arg2: int) -> None:
__init__is the initializer. It sets up instance state.-> Noneis a return type hint saying “this function returns nothing.” For__init__, that’s required: returning a value is an error.
1 | class Name: |
Mistakes
The syntax for i: int in ... is not allowed. This line:
1 | for i: int, x: int in enumerate(nums): |
is a SyntaxError in current Python versions. Python only allows annotations in:
function parameters / return types (
def f(x: int) -> str: ...)variable annotations / assignments (
index_of: dict[int, int] = {}orx: int)
but not in the target of a for loop. If we try it in a real interpreter, it fails. How to annotate those variables. We can annotate i and x, just not inside the for header:
1 | nums: list[int] = [1, 2, 3] |
This is legal Python: i: int and x: int are normal variable annotations.
But in practice, we almost never bother, because Type checkers usually infer loop variable types
If we annotate our function signature and main variables, the loop variables are obvious:
1 | def build_index(nums: list[int]) -> dict[int, int]: |
Given nums: list[int], a type checker will infer:
iisint(the index fromenumerate)xisint(the element type ofnums)
So extra annotations on i and x would just add noise.
Practical rule of thumb
Always annotate:
function parameters and return types
important attributes / module-level variables
tricky locals when the type isn’t obvious
Rarely annotate:
- simple loop variables (
i,x,iteminfor item in items)
- simple loop variables (
method
def method(self) -> str:
- A method is just a function defined inside a class; it receives the instance as the first parameter (by convention,
self). -> strmeans the method returns a string.
1 | class Name: |
if __name__ == "__main__": main()
This is the script entry-point guard.
- When we run the file directly:
python app.pyPython sets__name__ == "__main__"→main()executes. - When we import the file from somewhere else:
import appPython sets__name__ == "app"→ the guarded block does not run. This prevents side-effects (like starting a program) on import.
Example:
1 | # app.py |
This pattern keeps modules safe to import while still runnable as scripts.
Notes
a) What is __name__?
- Python sets a special variable
__name__in every module (file). - If we run a file directly (
python app.py), Python sets__name__to the string"__main__". - If we import that file (
import app), Python sets__name__to the module’s name, e.g."app"(or"package.app"if inside a package).
b) What does if __name__ == "__main__": do?
It’s a guard that says: only run this block when the file is executed as a script, not when it’s imported.
1 | # app.py |
- Run
python app.py→__name__ == "__main__"→ prints “Running as a script”. - From
other.pydoimport app→__name__ == "app"→ theifcondition is False, somain()is not called, and nothing prints. That’s by design, not an error.
This pattern prevents unwanted side effects when a module is imported.
c) Does __name__ need to match the class name (e.g., Name)?
No.
__name__refers to the module name (the filename, likeapp.py→"app").Nameis a class name inside the module. They are unrelated.
The code:
1 | from __future__ import annotations |
Here, __name__ is compared to "__main__" based on how the file is run, not the class name. We can have many classes/functions in the file; the guard is only about whether to auto-run main().
d) Tiny demo to see it
app.py
1 | def main() -> None: |
Direct run:
1
2
3$ python app.py
In app.py, __name__ is: __main__
Running as a scriptImported by another file:
other.py
1 | import app |
Run:
1 | $ python other.py |
Notice: when imported, __name__ becomes "app", so the guarded main() doesn’t execute.
Bottom line:
__name__is the module’s name, not a class name.- The
if __name__ == "__main__":guard runs code only when the file is executed directly, keeping imports clean and side-effect-free.