Literal, Final, ClassVar & Annotated
Four focused typing tools that go beyond plain type hints: Literal pins a value to specific constants, Final locks a name from reassignment, ClassVar marks class-level state, and Annotated attaches extra metadata to a type.
Beyond basic type hints like int or list[str], Python's typing module offers narrower, purpose-built annotations for common patterns: restricting a value to a fixed set of literals, declaring something should never be reassigned, distinguishing class-level attributes from instance attributes, and attaching arbitrary metadata to a type for tools to consume. Each is a static-typing construct — none of them are enforced by the Python runtime on their own.
Literal — Restricting to Specific Values
Literal[...] narrows a type hint to one or more exact, specific values rather than a whole type. It is ideal for string/int "mode" flags where only a fixed set of values is valid.
Like all typing constructs, Literal is checked statically. Passing "x" at runtime does not raise a TypeError by itself — Python still executes the call; only mypy/pyright catch the mismatch before runtime.
from typing import Literal
Mode = Literal["r", "w", "a", "rb", "wb"]
def open_file(path: str, mode: Mode) -> None:
print(f"opening {path} in mode {mode}")
open_file("data.txt", "r") # OK — "r" is one of the allowed literals
open_file("data.txt", "x") # flagged by a static checker — "x" not in ModeLiteral for Return Types and Overload Discrimination
Literal return types let checkers narrow downstream code based on a specific returned value, and are commonly paired with @overload to describe functions whose return type depends on a literal argument.
from typing import Literal
def status_code() -> Literal[200, 404, 500]:
return 200
code = status_code()
if code == 200:
print("success") # checker knows code is narrowed hereFinal — Preventing Reassignment
Final marks a name as intended to never be reassigned after its first assignment — for module-level constants, class attributes that subclasses shouldn't override, or instance attributes set once in __init__. Like other typing constructs, this is enforced only by static checkers, not by the Python runtime.
Final does not make an object immutable — it only prevents the NAME from being rebound. Final list data: Final = [1, 2] still allows data.append(3); it just forbids data = [4, 5].
from typing import Final
MAX_RETRIES: Final = 3
MAX_RETRIES = 5 # flagged by a static checker: cannot reassign a Final name
class Config:
VERSION: Final[str] = "1.0"
class DevConfig(Config):
VERSION = "1.0-dev" # flagged: cannot override a Final class attributeClassVar — Class-Level vs Instance-Level Attributes
ClassVar[...] tells a type checker that an attribute belongs to the class itself, shared across all instances, rather than being set per-instance. This is purely a typing annotation; it does not itself create the class variable — you still assign it in the class body as usual.
ClassVar is especially useful on dataclasses: a ClassVar-annotated field is excluded from the generated __init__, __repr__, and __eq__, since it isn't a per-instance field.
from typing import ClassVar
class Employee:
company_name: ClassVar[str] = "Acme Corp" # shared by all instances
name: str # per-instance attribute
def __init__(self, name: str) -> None:
self.name = name
e1 = Employee("Ana")
e2 = Employee("Ben")
print(e1.company_name, e2.company_name) # Acme Corp Acme Corp — same class attributefrom dataclasses import dataclass
from typing import ClassVar
@dataclass
class Counter:
count: int = 0
total_created: ClassVar[int] = 0 # not a dataclass field — no __init__ param
c = Counter()
print(c) # Counter(count=0) — total_created is not part of the generated reprAnnotated — Attaching Metadata to a Type
Annotated[Type, metadata...] lets you attach arbitrary extra metadata to a type hint without changing what the type actually is. The type checker still sees the plain Type for compatibility purposes; the metadata is available for other tools (validation libraries, documentation generators, dependency-injection frameworks) to inspect at runtime.
Metadata can be any object, not just a string — commonly a validation-library-specific marker object:
Inspect an Annotated type's metadata at runtime with typing.get_type_hints(fn, include_extras=True) or typing.get_args() — plain get_type_hints() strips the metadata by default.
from typing import Annotated
UserId = Annotated[int, "must be a positive integer"]
def get_user(user_id: UserId) -> str:
return f"user #{user_id}"
# The metadata string is descriptive only — Python does not enforce it.
# Third-party libraries (e.g. FastAPI, pydantic) read Annotated metadata
# to drive runtime validation, dependency injection, or documentation.from typing import Annotated
class Range:
def __init__(self, lo: int, hi: int) -> None:
self.lo, self.hi = lo, hi
Percentage = Annotated[int, Range(0, 100)]
def set_volume(level: Percentage) -> None:
print(f"volume set to {level}")Combining These Tools
These four constructs are frequently combined in real code — a class might use ClassVar for shared config, Final for a locked constant, Literal for a restricted mode field, and Annotated to document validation rules for a framework like pydantic or FastAPI.
from typing import ClassVar, Final, Literal, Annotated
class Server:
DEFAULT_PORT: Final[int] = 8080
active_connections: ClassVar[int] = 0
def __init__(self, mode: Literal["dev", "prod"], timeout: Annotated[int, "seconds"]) -> None:
self.mode = mode
self.timeout = timeoutPractical Usage and Rules
Use Literal for enum-like string/int flags where a full enum.Enum feels heavyweight. Use Final for constants and attributes that should never be reassigned by design. Use ClassVar whenever a class attribute is meant to be shared, not per-instance — this matters a lot in dataclasses. Use Annotated when a type hint alone can't express a validation rule or extra metadata a framework needs to consume.
None of Literal, Final, ClassVar, or Annotated's metadata are enforced by the Python interpreter at runtime on their own — they rely on a static type checker (or, for Annotated, a metadata-aware library) to have any effect.
Quick Reference
Literal["a", "b"] restrict to specific values
Final / Final[T] name must not be reassigned (static-only)
ClassVar[T] attribute is class-level, not per-instance
Annotated[T, meta...] type T, plus metadata for tools to inspectKey Takeaways
Literal[...] restricts a value to one of a fixed set of exact constants, checked statically.
Final marks a name as intended never to be reassigned; it does not make the referenced object itself immutable.
ClassVar[T] marks an attribute as class-level/shared rather than per-instance, and is excluded from dataclass-generated __init__.
Annotated[T, ...] attaches arbitrary metadata to a type hint that runtime-aware tools (like pydantic or FastAPI) can read via get_type_hints(include_extras=True).
All four are primarily static-typing tools — the interpreter itself does not enforce any of them without a checker or metadata-aware library involved.
data: Final = [1, 2, 3] data = [4, 5, 6] # checker error: cannot reassign Final name # but developers sometimes think Final also prevents this: data.append(4) # this actually succeeds — Final doesn't freeze the object from typing import Final data: Final = (1, 2, 3) # use an immutable type if you also need value immutability
Final only locks the NAME from reassignment; it does not make the referenced object immutable. Use an immutable type (tuple, frozenset) if the contents must also be unchangeable.
class Counter: count: int = 0 # looks shared, but it's just a mutable default via annotation def __init__(self): pass from typing import ClassVar class Counter: count: ClassVar[int] = 0 # explicit: genuinely shared across instances
Without ClassVar, a checker treats count: int = 0 ambiguously as a possible per-instance field default; ClassVar makes the shared, class-level intent explicit and unambiguous.
def get_user(user_id: Annotated[int, "positive"]) -> str: return f"user {user_id}" get_user(-5) # runs fine — no validation happens automatically
Annotated metadata is descriptive, not enforced by Python itself — a validation-aware library or explicit runtime check is required to actually act on it.
What does Literal["r", "w", "a"] restrict a value to, and is this checked at runtime or statically?
It restricts the value to exactly one of the strings 'r', 'w', or 'a'. This is checked statically by a type checker (mypy/pyright); Python itself does not enforce it at runtime.
Does marking a variable Final make the object it refers to immutable?
No. Final only prevents the NAME from being reassigned to a different object; if the object itself is mutable (like a list), its contents can still be changed in place.
Why would you annotate a dataclass field with ClassVar instead of leaving it as a normal field?
ClassVar tells both the type checker and the dataclass machinery that the attribute is shared at the class level, not a per-instance field — so it is excluded from the generated __init__, __repr__, and __eq__.
What is the purpose of Annotated[Type, metadata], and does Python enforce the metadata?
Annotated attaches arbitrary extra metadata to a type hint (for documentation, validation rules, or framework use) while the type checker still treats it as the plain Type. Python itself does not enforce the metadata; a metadata-aware library (like pydantic) or explicit code must act on it.
How do you read the metadata out of an Annotated type hint at runtime?
Use typing.get_type_hints(fn, include_extras=True) or typing.get_args() — plain get_type_hints() strips the Annotated metadata by default.
Can a subclass override a Final class attribute defined in its parent?
No — a static type checker flags this as an error, since Final class attributes are meant to be locked against being overridden by subclasses.
When might you prefer Literal over a full enum.Enum for a 'mode' parameter?
When the set of allowed values is small, string/int-based, and doesn't need enum's namespace, iteration, or extra methods — Literal gives static checking with less ceremony than defining a full Enum class.
Write a function set_log_level(level) typed with Literal to accept only "debug", "info", "warning", or "error", and call it with a valid value.
from typing import Literal LogLevel = Literal["debug", "info", "warning", "error"] def set_log_level(level: LogLevel) -> None: print(f"log level set to {level}") set_log_level("info")
Prints 'log level set to info'. A static checker would reject a call like set_log_level('verbose') since 'verbose' is not one of the declared Literal values, but Python itself would still run it without error.
What does this print?
3 3 3 is wrong — it prints 2 2 2. units_built is incremented via Robot.units_built (the class attribute) in each __init__ call; since it's shared, both instances see the same updated value of 2 after two Robot objects are created.
Write a module-level constant PI using Final, annotate a function parameter with Annotated to document that it expects 'radius in meters', and compute a circle's area.
from typing import Final, Annotated PI: Final[float] = 3.14159 def circle_area(radius: Annotated[float, "radius in meters"]) -> float: return PI * radius ** 2 print(round(circle_area(2.0), 2)) # 12.57
12.57 — PI is a locked constant (Final), and the Annotated metadata documents the parameter's meaning without changing how it's used or enforced at runtime.
from typing import ClassVar
class Robot:
units_built: ClassVar[int] = 0
def __init__(self, name: str) -> None:
self.name = name
Robot.units_built += 1
r1 = Robot("R1")
r2 = Robot("R2")
print(Robot.units_built, r1.units_built, r2.units_built)