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 Mode

Literal 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 here

Final — 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 attribute

ClassVar — 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 attribute
from 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 repr

Annotated — 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 = timeout

Practical 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 inspect

Key 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)