Skip to Content
Course content

314: Literal Types and Final Variables

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing code where the "contract" between two functions is basically a pinky-promise. You'll see a function that takes a string for a status or a mode, and if you look at the docstring, it says something like "Must be either 'read' or 'write'". The problem is that the Python interpreter doesn't care about your docstring, and neither does your IDE until the code actually crashes in production because someone typed "reade" instead of "read".

The fragility of implied strings

Let's look at how we usually handle this when we're moving too fast. Imagine we're building a simple file processor. We might write something like this:

def open_connection(mode: str):
    if mode == "read":
        print("Opening for reading...")
    elif mode == "write":
        print("Opening for writing...")
    else:
        raise ValueError(f"Invalid mode: {mode}")

# Somewhere else in the app
open_connection("reade")  # Typo! 

This code is technically correct, but it's fragile. The str type hint is too broad. It tells the developer (and the linter) that any string is acceptable, but the logic inside the function proves that only two specific strings are actually valid. You're forcing the runtime to do the validation work that your static analysis tools should have caught while you were still typing.

Enforcing specific values with Literal

This is where Literal comes in. Instead of telling Python "this is a string," we tell it "this is exactly one of these specific strings." It shifts the error from a runtime ValueError to a red squiggle in your editor.

from typing import Literal

# We define a type alias to keep things clean
FileMode = Literal["read", "write"]

def open_connection(mode: FileMode):
    if mode == "read":
        print("Opening for reading...")
    elif mode == "write":
        print("Opening for writing...")

open_connection("reade")  # Your IDE/Mypy will flag this immediately

I prefer this because it serves as living documentation. A new engineer joining the project doesn't have to hunt through the function body to see what the valid options are; they just hover over the FileMode type. It’s a massive win for maintainability, especially when you have five or six valid options instead of just two.

The illusion of constants

While we're tightening up our types, let's talk about constants. In Python, we've all been taught to name things in UPPER_CASE to signal "don't touch this." But as you know, that's just a convention. There is nothing stopping a tired developer from accidentally reassigning a global configuration value halfway through a script.

MAX_RETRIES = 3

def reset_config():
    # Someone thinks they're being clever or just makes a mistake
    global MAX_RETRIES
    MAX_RETRIES = 0 

In a small script, this is a nuisance. In a large distributed system, changing a global timeout or retry limit at runtime can cause some truly haunting bugs that are nearly impossible to reproduce in staging.

Locking the door with Final

If you want to tell your teammates (and your future self) that a variable is absolutely not meant to be changed, use Final. It doesn't stop the code from running if you ignore it—Python is still Python—but it tells the type checker to scream if a reassignment is attempted.

from typing import Final

MAX_RETRIES: Final = 3

def reset_config():
    MAX_RETRIES = 0  # Type checker error: Cannot assign to Final name "MAX_RETRIES"

I use Final whenever I define API keys, database ports, or mathematical constants. It removes the ambiguity. When I see Final, I know I can rely on that value remaining constant throughout the entire lifecycle of the process without having to scan the rest of the file for rogue assignments.




📋 Practical Task

Refactoring the API Gateway Configuration

You've inherited a piece of code for an API Gateway. It's currently plagued by "magic strings" and accidental constant overrides. Your task is to harden this code using Literal and Final.

Requirements:

  • Create a type alias called GatewayPriority that only allows the values "High", "Medium", or "Low".
  • Update the set_priority function to use this new type.
  • Mark the GATEWAY_PORT variable as Final to ensure it isn't changed during runtime.
  • Correct the typo in the function call at the bottom of the script so that it passes type checking.
from typing import Final, Literal

# TODO: Mark this as Final
GATEWAY_PORT = 8080

# TODO: Create the GatewayPriority Literal type here

def set_priority(priority: str):
    print(f"Setting gateway priority to {priority}")

# This should be flagged as an error by a type checker
set_priority("Higg") 

# Test the port assignment (this should be flagged as an error)
GATEWAY_PORT = 9000
Rating
0 0

There are no comments for now.

to be the first to leave a comment.