Sometimes "a string" isn't precise enough — you want to say "exactly one of these strings." Literal["red", "green", "blue"] as a parameter type means the type checker only accepts those three values. Useful for status enums-as-strings, mode flags, anywhere the choice is from a fixed set.
Final — this won't be reassigned
Final tells the type checker that a variable, attribute, or class is not meant to be reassigned or subclassed. MAX_SIZE: Final = 100. MAX_SIZE = 200 later in the module is a type error. Useful for constants and sentinels — communicates intent and lets the type checker enforce it.
ClassVar — this is a class attribute, not an instance one
In a class body, x: int is by default treated as an instance attribute (something every instance has). x: ClassVar[int] tells the type checker it's a class attribute (shared across instances). Especially important in dataclasses where the distinction is meaningful — class-level constants vs per-instance fields.
Annotated — attaching metadata to types
Annotated[int, "the user id"] is the same type as int at runtime, but with metadata attached. Used heavily by Pydantic, FastAPI, and validation libraries to encode constraints (min/max, regex, etc.) without inventing new type aliases.
Pythonic Way: Reach for Literal when a string-or-int is actually one of a fixed set of choices. The type checker error you get from a typo ("warming" instead of "warning") is a real bug-prevention win. Final on constants makes refactor mistakes louder.
Code
Literal — exact values·python
from typing import Literal
Mode = Literal["r", "w", "a", "r+"]
def open_file(path: str, mode: Mode = "r") -> str:
return f"opening {path} in {mode}"
open_file("x.txt", "r") # OK
open_file("x.txt", "w") # OK
# open_file("x.txt", "x") # mypy error: Literal['r', 'w', 'a', 'r+'] expected
# Literals work for ints, bools, None too
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
MaybeOn = Literal[True, False]
Final — non-reassignable·python
from typing import Final
# Module-level constant
MAX_RETRIES: Final = 3
# MAX_RETRIES = 5 # mypy error: Cannot assign to final name
# Class attribute
class Config:
VERSION: Final = "1.0.0"
TIMEOUT: Final[int] = 30
# Subclassing also flagged
# class SubConfig(Config):
# VERSION = "2.0" # mypy error: Cannot override final attribute
# Useful for sentinels, configuration constants, anything immutable by intent
ClassVar — class attribute vs instance·python
from typing import ClassVar
from dataclasses import dataclass
@dataclass
class User:
name: str # instance attribute
age: int # instance attribute
DEFAULT_ROLE: ClassVar[str] = "member" # class attribute, shared
u = User("alice", 30)
print(u.name)
print(u.DEFAULT_ROLE) # 'member' — accessed via instance
print(User.DEFAULT_ROLE) # also via class
# Without ClassVar, dataclass would treat DEFAULT_ROLE as an instance field
# and try to require it in __init__ — wrong behavior.
Annotated — type + metadata·python
from typing import Annotated
# Plain int — no extra info
def plain(x: int) -> int:
return x
# Annotated int with constraints
UserId = Annotated[int, "unique identifier for a user", "positive integer"]
def with_meta(uid: UserId) -> str:
return f"user {uid}"
# At runtime, UserId is just an int
print(with_meta(42)) # 'user 42'
# The metadata is accessible via typing.get_type_hints / typing.get_args
from typing import get_type_hints, get_args
hints = get_type_hints(with_meta, include_extras=True)
print(hints["uid"])
print(get_args(hints["uid"]))
Define a LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]. Write a function log(message: str, level: LogLevel = "INFO") that just prints. Then define a class Config with a Final attribute VERSION: Final = "1.0.0" and a ClassVar[int]MAX_RETRIES: ClassVar[int] = 3. Show that the type-checker complains if you try to reassign VERSION or pass an unsupported log level.
Progress
Progress is local-only — sign in to sync across devices.