Introduction to Typing Extensions in Python
The typing-extensions
library is a powerful addition to Python’s type hinting system, enabling developers to use advanced and future-ready typing features that aren’t available in their current version of Python. This library plays a crucial role in improving code quality, maintainability, and readability by extending the typing support. In this article, we will explore some of the most practical and useful APIs in typing-extensions
with real-world examples. We’ll also build a small app utilizing these features.
Why Typing Extensions?
Python’s built-in typing
module contains a set of core type hints for static analysis. However, language features and typing constructs evolve over time. The typing-extensions
package backports new type hints from future Python versions into older ones so that developers can keep their codebase modern without waiting for a Python upgrade.
Dozens of Essential Typing Extensions APIs
1. Literal
The Literal
type allows you to specify that a variable or parameter must have one or more specific values.
from typing_extensions import Literal def get_status_message(status: Literal["success", "error", "pending"]) -> str: if status == "success": return "Operation successful!" elif status == "error": return "An error occurred." elif status == "pending": return "Still in progress." return "Unknown status" # Example usage print(get_status_message("success")) # Outputs: "Operation successful!"
2. TypedDict
TypedDict
allows you to define dictionaries with a specific structure for their keys and values.
from typing_extensions import TypedDict class User(TypedDict): id: int username: str email: str def display_user(user: User) -> None: print(f"ID: {user['id']}, Username: {user['username']}, Email: {user['email']}") # Example usage user_data = {"id": 123, "username": "johndoe", "email": "john@example.com"} display_user(user_data) # Outputs: ID: 123, Username: johndoe, Email: john@example.com
3. Protocol
With Protocol
, you can define structural types, specifying the shape an object must have rather than its specific type.
from typing_extensions import Protocol class Flyer(Protocol): def fly(self) -> str: ... class Bird: def fly(self) -> str: return "Bird is flying" class Airplane: def fly(self) -> str: return "Airplane is flying" def display_flight(flyer: Flyer) -> str: return flyer.fly() # Example usage bird = Bird() airplane = Airplane() print(display_flight(bird)) # Outputs: Bird is flying print(display_flight(airplane)) # Outputs: Airplane is flying
4. Concatenate
Concatenate
supports more complex function signatures when used with Callable
.
from typing_extensions import Callable, Concatenate, ParamSpec P = ParamSpec("P") def log_and_call(func: Callable[Concatenate[str, P], str], msg: str, *args: P.args, **kwargs: P.kwargs) -> str: print(f"Log: {msg}") return func(msg, *args, **kwargs) def sample_function(log_prefix: str, name: str) -> str: return f"{log_prefix}: Hello, {name}!" # Example usage print(log_and_call(sample_function, "INFO", "Alice")) # Outputs: Log: INFO \n INFO: Hello, Alice!
5. @final
The @final
decorator marks a method or class as “final,” preventing it from being overridden or subclassed.
from typing_extensions import final @final class Singleton: pass # This will raise an error # class Derived(Singleton): pass class Base: @final def important_method(self) -> None: print("This method cannot be overridden") # This will raise an error # class Derived(Base): # def important_method(self) -> None: pass
A Small App Example with Typing Extensions
Below is a small example showcasing several of these APIs together.
from typing_extensions import Literal, TypedDict, Protocol, final class Product(TypedDict): id: str name: str price: float status: Literal["available", "unavailable"] class DiscountCalculator(Protocol): def calculate(self, price: float, discount: float) -> float: ... def get_product_status(product: Product) -> str: if product["status"] == "available": return f"{product['name']} is available for purchase" else: return f"{product['name']} is currently unavailable" class BasicDiscount: def calculate(self, price: float, discount: float) -> float: return price - (price * discount / 100) @final class CheckoutSystem: def process(self, product: Product, discount_calculator: DiscountCalculator) -> None: status = get_product_status(product) print(status) if product["status"] == "available": discounted_price = discount_calculator.calculate(product["price"], 10) print(f"Discounted price: {discounted_price}") # Example usage product = {"id": "1234", "name": "Laptop", "price": 1200.99, "status": "available"} discount = BasicDiscount() checkout = CheckoutSystem() checkout.process(product, discount)
This small app uses TypedDict
for the product structure, Protocol
for the discount calculator interface, Literal
for specifying valid statuses, and @final
to ensure that the checkout system cannot be subclassed.
Conclusion
The typing-extensions
package is a must-have for any Python developer looking to create scalable, future-proof, and maintainable code. By adopting these advanced type hinting techniques, you can significantly improve the reliability and readability of your codebase.