Comprehensive Guide to Typing Extensions for Python Developers

Introduction to typing-extensions

The typing-extensions library is the go-to package for Python developers who want to experiment with typing features not yet available in the standard typing module. It provides compatibility for older Python versions and serves as a testing ground for new type hints before they get integrated into the standard library. Let’s dive deep into this useful library and understand how to leverage its features in your Python projects.

Why Use typing-extensions?

While the typing module is continually evolving, not all the updated typing features make it to earlier Python versions. typing-extensions ensures backward compatibility and allows experimentation with future typing constructs. Below, we explore its most notable APIs through examples.

Dozens of Useful APIs in typing-extensions

1. Literal

The Literal type is used to allow specific constant values. This is useful in functions where the argument is expected to be from a predefined set of values.

  from typing_extensions import Literal

  def greet(language: Literal['en', 'es', 'fr']) -> str:
      translations = {'en': 'Hello', 'es': 'Hola', 'fr': 'Bonjour'}
      return translations[language]

  print(greet('en'))  # Output: Hello

2. TypedDict

TypedDict enables defining dictionaries with specific types for keys and values.

  from typing_extensions import TypedDict

  class User(TypedDict):
      name: str
      age: int

  user: User = {'name': 'Alice', 'age': 30}
  print(user)  # Output: {'name': 'Alice', 'age': 30}

3. Final

Final enforces immutability by indicating that the variable or method cannot be overridden.

  from typing_extensions import Final

  PI: Final = 3.14159
  # PI = 3.14  # This will raise a mypy error

4. Protocol

Protocol is used for structural subtyping, allowing you to specify required methods and properties for a type.

  from typing_extensions import Protocol

  class Greeter(Protocol):
      def greet(self) -> None:
          ...

  class Person:
      def greet(self) -> None:
          print("Hello!")

  def say_hello(greeter: Greeter) -> None:
      greeter.greet()

  person = Person()
  say_hello(person)  # Output: Hello!

5. Annotated

Annotated can be used for adding metadata to types.

  from typing_extensions import Annotated

  def process_data(data: Annotated[str, "Input must be a string"]) -> str:
      return data.upper()

  print(process_data("example"))  # Output: EXAMPLE

6. Self

Self is a convenient way to annotate methods that return the same instance type.

  from typing_extensions import Self

  class FluentBuilder:
      def __init__(self):
          self.result = ""

      def add(self, text: str) -> Self:
          self.result += text
          return self

      def build(self) -> str:
          return self.result

  builder = FluentBuilder()
  print(builder.add("Hello ").add("World!").build())  # Output: Hello World!

7. Unpack

Unpack is used for unpacking tuple or list types into distinct variables in type hints.

  from typing_extensions import Unpack
  from typing import TypedDict

  class Point(TypedDict):
      x: int
      y: int

  def print_point(**point: Unpack[Point]) -> None:
      print(f"x: {point['x']}, y: {point['y']}")

  print_point(x=10, y=20)  # Output: x: 10, y: 20

8. @runtime_checkable

Allows Protocol to be used in runtime type checking with isinstance or issubclass.

  from typing_extensions import Protocol, runtime_checkable

  @runtime_checkable
  class Movable(Protocol):
      def move(self) -> None:
          ...

  class Car:
      def move(self) -> None:
          print("The car is moving")

  car = Car()
  if isinstance(car, Movable):
      car.move()  # Output: The car is moving

Building an App Using typing-extensions APIs

Here’s an example of a basic task management app that leverages TypedDict, Literal, and Annotated.

  from typing_extensions import TypedDict, Literal, Annotated
  from datetime import datetime


  class Task(TypedDict):
      id: int
      title: str
      status: Literal['todo', 'in-progress', 'done']
      created_at: Annotated[datetime, "Creation timestamp"]


  tasks: list[Task] = []


  def add_task(title: str, status: Literal['todo', 'in-progress', 'done'] = 'todo') -> None:
      task: Task = {
          'id': len(tasks) + 1,
          'title': title,
          'status': status,
          'created_at': datetime.now()
      }
      tasks.append(task)


  def list_tasks() -> None:
      for task in tasks:
          print(f"{task['id']} - {task['title']} ({task['status']})")


  add_task("Write blog post", "todo")
  add_task("Review PR", "in-progress")
  list_tasks()
  # Output:
  # 1 - Write blog post (todo)
  # 2 - Review PR (in-progress)

Conclusion

The typing-extensions library offers powerful tools to augment your Python development, especially when you require features not yet integrated into the default typing module. With its compatibility and flexibility, it ensures your codebase remains robust as Python evolves. Start integrating these features today!

Leave a Reply

Your email address will not be published. Required fields are marked *