Right, let’s talk about the typing module’s attempt to impose some order on the delightful chaos of Python. We’ve covered the basics, but sometimes you need to be more specific than int or str. Sometimes you need to tell the type checker, “No, I don’t mean any string, I mean this specific string.” Enter Literal, Final, and ClassVar—the module’s tools for pedants who like their intentions crystal clear (and I count myself among them).

Literal: Because Sometimes ‘str’ Isn’t Specific Enough

Imagine you’re writing a function to control a traffic light. A bad way to do it would be to accept a string:

def set_light_signal(signal: str) -> None:
    ...

What’s to stop some maniac from calling set_light_signal("maroon")? The function would break, and traffic would be a mess. You could use an Enum, and that’s often a great choice. But for small, fixed sets of primitive values, Literal is a wonderfully lightweight alternative. It lets you say, “This argument must be one of these exact values.”

from typing import Literal

def set_light_signal(signal: Literal["red", "yellow", "green"]) -> None:
    ...

# These are fine
set_light_signal("red")
set_light_signal("green")

# Your type checker will (rightfully) lose its mind here
set_light_signal("blue")  # Error: Argument of type "blue" cannot be assigned to parameter "signal"

It’s not just for strings. You can use it with any literal bool, int, float, or None. The key thing to remember is that it’s about value equality. The type checker isn’t doing fancy inference; it’s checking if the value you provided is literally one of the ones you listed. This makes it perfect for APIs that use string or integer flags.

A common pitfall? Thinking it works with variables. It doesn’t, and it shouldn’t.

red_val = "red"
set_light_signal(red_val)  # Type Checker: "I see a 'str', not a Literal['red']"

The variable red_val is of type str, and its value could be changed at runtime. Literal is a static, compile-time guarantee. If you need to use a variable, you’re probably looking for an Enum.

Final: The “Do Not Touch” Decoration

Ever had a variable you declared as a constant, only to find some other part of your codebase has “helpfully” updated it? Final is your way of telling the type checker and any future developers (including future you), “This is done. Do not reassign this. I am not asking.”

from typing import Final

MAX_CONNECTIONS: Final = 10
SERVER_URL: Final[str] = "https://api.my-awesome-app.com"

# This will make your type checker throw a flag
MAX_CONNECTIONS = 20  # Error: Cannot assign to final name "MAX_CONNECTIONS"

It’s called a “decorator,” but you use it as an annotation. It works on module-level variables, class attributes, and instance attributes. The most important thing to understand is that Final is only enforced by the type checker. Python itself at runtime doesn’t care. It’s a declarative statement of intent, not a lock and key. It’s you saying, “I intend for this to never change,” and the type checker making sure your code doesn’t accidentally violate that intention.

You can also use it with methods to prevent overriding in subclasses, which is incredibly useful for designing stable class hierarchies.

class Transport:
    def get_protocol(self) -> str:
        return "HTTP/1.1"

class MyTransport(Transport):
    def get_protocol(self) -> str:  # This is fine, for now.
        return "HTTP/3"

# Now let's make it Final
class SecureTransport:
    @final
    def get_encryption_cipher(self) -> str:
        return "AES-256"

class MySecureTransport(SecureTransport):
    def get_encryption_cipher(self) -> str:  # Error: Method is final and cannot be overridden
        return "my-weaker-cipher"  # Nope, not on my watch.

ClassVar: Ending the Instance Attribute Ambiguity

Here’s a classic point of confusion in Python classes:

class Server:
    # Is this a class-level default or an instance-level default?
    timeout: int = 10

The answer is: it’s both. You can access it via Server.timeout and also via self.timeout. And if you set self.timeout = 20, you’ve now created an instance attribute that shadows the class attribute. This is a common source of subtle bugs.

ClassVar exists to end this ambiguity. It explicitly says, “This variable belongs to the class, not to instances of the class.” It’s a declaration of intent that the type checker enforces.

from typing import ClassVar

class Server:
    # This is explicitly a class attribute
    default_timeout: ClassVar[int] = 10
    # This is an instance attribute, with a default value
    instance_timeout: int = 5

# This is correct and intended usage
print(Server.default_timeout)  # 10

s = Server()
print(s.instance_timeout)      # 5
print(s.default_timeout)       # Also 10, because Python allows this lookup

# But the type checker knows the difference
s.default_timeout = 30  # Error: Cannot assign to class variable "default_timeout" via instance

The key takeaway: Use ClassVar when the data is meant to be shared across all instances and should not be set on a per-instance basis. It doesn’t change runtime behavior—Python will still let you do the bad thing—but your type checker will now have your back, catching a whole category of bugs before they happen. It turns a common ambiguity into an explicit, checked contract.