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
2
3
from __future__ import annotations
# Stores type annotations as strings (postponed evaluation).
# Useful for forward references and avoiding import cycles.

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
    5
    from __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
2
def parse(x: int | str) -> int:  # x can be int OR str
return int(x)

What Node | None = None means?

Inside a class like a linked-list node:

1
2
3
4
5
from __future__ import annotations  # not needed on Python 3.11+

class Node:
def __init__(self, next: Node | None = None) -> None:
self.next = next
  • Node | None → the parameter next may be either a Node or None (i.e., it’s optional / nullable).
  • = None → if we don’t pass a value, the default is None.
  • So the constructor allows Node(), or Node(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.
  • -> None is a return type hint saying “this function returns nothing.” For __init__, that’s required: returning a value is an error.
1
2
3
4
class Name:
def __init__(self, arg1: str, arg2: int) -> None:
self.attr1 = arg1
self.attr2 = arg2

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).
  • -> str means the method returns a string.
1
2
3
4
5
6
7
8
class Name:
def __init__(self, arg1: str, arg2: int) -> None:
self.attr1 = arg1
self.attr2 = arg2

def method(self) -> str:
"""Return the primary attribute as a string."""
return self.attr1

if __name__ == "__main__": main()

This is the script entry-point guard.

  • When we run the file directly: python app.py Python sets __name__ == "__main__"main() executes.
  • When we import the file from somewhere else: import app Python sets __name__ == "app" → the guarded block does not run. This prevents side-effects (like starting a program) on import.

Example:

1
2
3
4
5
6
7
8
# app.py
def main() -> None:
print("Running as a script")

if __name__ == "__main__":
main()
# other.py
import app # does NOT print anything, because __name__ != "__main__"

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
2
3
4
5
6
# app.py
def main() -> None:
print("Running as a script")

if __name__ == "__main__":
main()
  • Run python app.py__name__ == "__main__" → prints “Running as a script”.
  • From other.py do import app__name__ == "app" → the if condition is False, so main() 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, like app.py"app").
  • Name is a class name inside the module. They are unrelated.

The code:

1
2
3
4
5
6
7
8
from __future__ import annotations

class Name:
"""Simple class holding two attributes."""
...

if __name__ == "__main__":
main()

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
2
3
4
5
6
7
def main() -> None:
print("Running as a script")

print(f"In app.py, __name__ is: {__name__}")

if __name__ == "__main__":
main()
  • Direct run:

    1
    2
    3
    $ python app.py
    In app.py, __name__ is: __main__
    Running as a script
  • Imported by another file:

other.py

1
2
import app
print("Imported app; not running its main().")

Run:

1
2
3
$ python other.py
In app.py, __name__ is: app
Imported app; not running its main().

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.