C.W.K.
Stream
Lesson 02 of 05 · published

Literal, Final, ClassVar — Precise Constraints

~18 min · literal, final, classvar, annotated

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Literal — exact value, not just type

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"]))

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.