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