Overview

Python 3.7, released on June 27, 2018, is a particularly well-loved release. It introduces dataclasses, an elegant tool for creating data-holding classes without the usual boilerplate. The breakpoint() function simplifies debugging, and the contextvars module provides thread-safe context variables.

This version also makes dictionary insertion order part of the language specification. With postponed evaluation of annotations and importlib.resources, Python 3.7 strengthens the foundations laid by previous releases.

Major Features

dataclasses (PEP 557)

The @dataclass decorator automatically generates __init__, __repr__, __eq__, and other methods from class annotations. It is a lighter and more flexible alternative to namedtuple and manually written classes.

python
from dataclasses import dataclass, field
from typing import List

# Basic dataclass
@dataclass
class InventoryItem:
    name: str
    unit_price: float
    quantity: int = 0

    @property
    def total_value(self) -> float:
        return self.unit_price * self.quantity

keyboard = InventoryItem("Mechanical keyboard", 89.99, 15)
mouse = InventoryItem("Ergonomic mouse", 49.99, 30)
print(keyboard)
# InventoryItem(name='Mechanical keyboard', unit_price=89.99, quantity=15)
print(f"Keyboard stock value: {keyboard.total_value:.2f} EUR")
# Keyboard stock value: 1349.85 EUR

# Automatic equality
copy = InventoryItem("Mechanical keyboard", 89.99, 15)
print(keyboard == copy)  # True

# field() and default_factory for mutable types
@dataclass
class Order:
    customer: str
    items: List[InventoryItem] = field(default_factory=list)
    reference: str = field(default="", repr=False)
    _total_cache: float = field(default=0.0, init=False, repr=False)

    def add(self, item: InventoryItem) -> None:
        self.items.append(item)

    @property
    def total(self) -> float:
        return sum(i.total_value for i in self.items)

order = Order("Dupont SA")
order.add(keyboard)
order.add(mouse)
print(f"Order total: {order.total:.2f} EUR")
# Order total: 2849.55 EUR

# Dataclass inheritance
@dataclass
class PerishableItem(InventoryItem):
    expiry_date: str = ""
    max_temperature: float = 4.0

yogurt = PerishableItem("Organic yogurt", 1.20, 200, "2024-03-15", 6.0)
print(yogurt)
# PerishableItem(name='Organic yogurt', unit_price=1.2,
#   quantity=200, expiry_date='2024-03-15', max_temperature=6.0)

# Comparison with namedtuple
from collections import namedtuple

# namedtuple: immutable, no simple default values
Point2D = namedtuple("Point2D", ["x", "y"])
p = Point2D(3, 4)
# p.x = 5  # AttributeError: immutable!

# dataclass: mutable by default, default values, methods
@dataclass
class Point3D:
    x: float = 0.0
    y: float = 0.0
    z: float = 0.0

p3 = Point3D(3, 4)
p3.z = 5  # Works!
print(p3)  # Point3D(x=3, y=4, z=5)

breakpoint() (PEP 553)

The built-in breakpoint() function simplifies launching the debugger. Instead of manually importing pdb and calling pdb.set_trace(), a simple call to breakpoint() is enough. The added benefit is control via the PYTHONBREAKPOINT environment variable.

python
# Before Python 3.7
# import pdb; pdb.set_trace()  # Tedious to type

# Python 3.7+: simple and configurable
def find_duplicates(elements):
    """Find duplicates in a list."""
    seen = set()
    duplicates = []
    for elem in elements:
        if elem in seen:
            # breakpoint()  # Uncomment to debug here
            duplicates.append(elem)
        seen.add(elem)
    return duplicates

data = ["alpha", "beta", "gamma", "beta", "delta", "alpha"]
print(find_duplicates(data))  # ['beta', 'alpha']

# Control via PYTHONBREAKPOINT:
#
# Disable all breakpoints:
#   PYTHONBREAKPOINT=0 python my_script.py
#
# Use a different debugger (e.g., ipdb):
#   PYTHONBREAKPOINT=ipdb.set_trace python my_script.py
#
# Use a web debugger (e.g., web-pdb):
#   PYTHONBREAKPOINT=web_pdb.set_trace python my_script.py

contextvars (PEP 567)

The contextvars module provides context variables that are implicitly passed through asynchronous calls. Unlike global variables or threading.local(), ContextVar objects work correctly with asyncio and coroutines.

python
import contextvars
import asyncio

# Context variable for tracking request IDs
request_id: contextvars.ContextVar[str] = contextvars.ContextVar(
    "request_id", default="unknown"
)

# Utility function that uses the context
def log_message(level, message):
    """Log a message with the current request ID."""
    rid = request_id.get()
    print(f"[{level}] [{rid}] {message}")

# Simulating async request processing
async def process_request(rid, data):
    """Process a request, propagating its ID in the context."""
    request_id.set(rid)
    log_message("INFO", f"Starting processing of '{data}'")
    await asyncio.sleep(0.1)  # Simulate processing
    log_message("INFO", f"Processing complete for '{data}'")

async def server():
    """Simulate a server handling multiple requests."""
    await asyncio.gather(
        process_request("REQ-001", "home page"),
        process_request("REQ-002", "user profile"),
        process_request("REQ-003", "product list"),
    )

# asyncio.run(server())
# [INFO] [REQ-001] Starting processing of 'home page'
# [INFO] [REQ-002] Starting processing of 'user profile'
# [INFO] [REQ-003] Starting processing of 'product list'
# [INFO] [REQ-001] Processing complete for 'home page'
# ...

# Copy context to isolate modifications
ctx = contextvars.copy_context()
# Modifications inside ctx do not affect the current context

Postponed evaluation of annotations (PEP 563)

With from __future__ import annotations, type annotations are no longer evaluated at function definition time but are stored as strings. This solves the circular reference problem and improves startup performance.

python
from __future__ import annotations

# Without this import, this class would raise a NameError
# because Node is not yet defined when it is used
class Node:
    """Binary tree node with forward references."""
    def __init__(self, value: int,
                 left: Node | None = None,
                 right: Node | None = None):
        self.value = value
        self.left = left
        self.right = right

    def insert(self, value: int) -> Node:
        """Insert a value and return the modified node."""
        if value < self.value:
            if self.left is None:
                self.left = Node(value)
            else:
                self.left.insert(value)
        else:
            if self.right is None:
                self.right = Node(value)
            else:
                self.right.insert(value)
        return self

# Cross-references between classes
class Company:
    name: str
    employees: list[Employee]  # Forward reference

class Employee:
    name: str
    employer: Company  # Back reference

# Annotations are accessible as strings
print(Node.__init__.__annotations__)
# {'value': 'int', 'left': 'Node | None', ...}

importlib.resources

The importlib.resources module provides a clean API for accessing data files bundled within Python packages. It is a modern and reliable alternative to pkg_resources (setuptools), which is heavy and slow to import.

python
# Package structure:
# my_package/
#   __init__.py
#   data/
#     default_config.json
#     schema.sql

# Old style with pkg_resources (slow, deprecated)
# import pkg_resources
# path = pkg_resources.resource_filename('my_package', 'data/config.json')

# New style with importlib.resources
from importlib import resources

# Read a text file from a package
# content = resources.read_text('my_package.data', 'default_config.json')

# Read a binary file
# raw = resources.read_binary('my_package.data', 'schema.sql')

# Use a temporary file (for C libraries, etc.)
# with resources.path('my_package.data', 'schema.sql') as filepath:
#     print(f"Temporary file: {filepath}")
#     # Use filepath as a real file on disk

# List available resources
# files = resources.contents('my_package.data')
# print(list(files))

Minor Improvements

  • Dictionary insertion order is now guaranteed by the language specification (no longer just a CPython implementation detail).
  • The asyncio module gains asyncio.run(), a simplified entry point.
  • The time module adds nanosecond functions: time.time_ns(), time.perf_counter_ns(), etc.
  • Dictionaries support reverse iteration with reversed().
  • Significant improvement in interpreter startup time (10 to 30% faster).

Deprecations and Removals

Notable changes:

  • The asyncio module deprecates callback-based functions in favor of coroutines.
  • async and await officially become reserved keywords.
  • StopIteration inside generators now raises a RuntimeError (PEP 479).
  • Implicit asyncio event loops are deprecated.

Sources