What Is New in Python 3.7
Python 3.7 was released on June 27, 2018. The defining additions are the dataclasses module, contextvars for async context propagation, breakpoint() as a standardized debugger entry point, postponed annotation evaluation, and nanosecond-precision time functions. CPython's startup time also improved significantly.
| Category | Change | PEP / Reference |
|---|---|---|
| New Syntax | Postponed evaluation of type annotations via from __future__ import annotations |
PEP 563 |
| New Modules | dataclasses -- auto-generated __init__, __repr__, __eq__ for classes |
PEP 557 |
| New Modules | contextvars -- context-local state for coroutines and threads |
PEP 567 |
| New Modules | importlib.resources -- access package data files without __file__ hacks |
-- |
| New Built-ins | breakpoint() -- configurable debugger entry point |
PEP 553 |
| Language | async and await become reserved keywords |
-- |
| Language | Dict insertion order is now a guaranteed language spec (not just CPython impl detail) | -- |
| Performance | Nanosecond-resolution time functions (time.time_ns(), etc.) |
PEP 564 |
| Performance | Faster startup time -- attribute lookup improvements | -- |
| Interpreter | Hash-based .pyc files for reproducible builds |
PEP 552 |
| Interpreter | UTF-8 mode (-X utf8) and legacy C locale coercion |
PEP 538, PEP 540 |
| Interpreter | Development mode (-X dev) with extra runtime checks |
-- |
| asyncio | Major overhaul -- new high-level API, running coroutines is now asyncio.run() |
-- |
What Are the Key New Features in Python 3.7?
dataclasses -- Boilerplate-Free Classes (PEP 557)
The @dataclass decorator auto-generates __init__, __repr__, __eq__, and optionally __lt__, __hash__, and __post_init__ based on class-level type annotations. This is the single most adopted stdlib addition of Python 3.7.
from dataclasses import dataclass, field
@dataclass(order=True)
class Point:
x: float
y: float
label: str = field(default="origin", compare=False)
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0, label="home")
print(p1 == p2) # True -- label excluded from comparison
print(repr(p1)) # Point(x=1.0, y=2.0, label='origin')
You control which fields participate in comparison, hashing, and repr independently. For mutable default values, use field(default_factory=list) -- not a bare [].
contextvars -- Context-Local State (PEP 567)
Context variables are the async-safe replacement for thread-local storage. Each coroutine runs in its own context copy, so setting a ContextVar inside a coroutine doesn't bleed into sibling coroutines sharing the same thread.
from contextvars import ContextVar
request_id: ContextVar[str] = ContextVar("request_id", default="none")
async def handle_request(rid: str):
token = request_id.set(rid)
await do_work() # request_id is "rid" here, not visible to other coroutines
request_id.reset(token)
async def do_work():
print(request_id.get()) # prints the request-specific value
breakpoint() -- Pluggable Debugger (PEP 553)
Instead of importing pdb and calling pdb.set_trace(), you now just call breakpoint(). The actual debugger is configurable via the PYTHONBREAKPOINT environment variable -- set it to a dotted import path, or to 0 to disable all breakpoints without touching code.
# In code
breakpoint() # drops into pdb by default
# To use a different debugger:
# PYTHONBREAKPOINT=pudb.set_trace
# To silence all breakpoints:
# PYTHONBREAKPOINT=0
How Did asyncio Change in Python 3.7?
Python 3.7 is where asyncio became genuinely pleasant to use. The biggest change: asyncio.run(coro) -- a single call to run a top-level coroutine, create an event loop, and clean it up automatically. Previously this required four or five lines of boilerplate.
import asyncio
async def main():
print("hello")
await asyncio.sleep(1)
print("world")
asyncio.run(main()) # Python 3.7+
Other asyncio additions: asyncio.create_task() replaces the verbose loop.create_task(), asyncio.get_running_loop() raises RuntimeError if no loop is active (safer than get_event_loop()), and asyncio.current_task() / asyncio.all_tasks() for task introspection.
Postponed Annotation Evaluation (PEP 563)
With from __future__ import annotations, all annotations are stored as strings (lazily) rather than evaluated at definition time. This solves forward reference problems and reduces import-time overhead for annotation-heavy code.
from __future__ import annotations
class Node:
def next(self) -> Node: # Forward reference -- works without quotes
...
def clone(self) -> Node:
...
Without the import, Node would not yet be defined at class body evaluation time. Postponed evaluation was planned to become the default in 3.10, but that change was deferred indefinitely due to compatibility concerns with runtime annotation consumers.
Performance Improvements and CPython Changes
- Startup time improved -- method resolution and attribute lookup are faster, particularly for classes with many inherited attributes.
- Six new nanosecond-precision time functions:
time.time_ns(),time.perf_counter_ns(),time.monotonic_ns(), etc. These avoid floating-point precision loss on high-resolution clocks. - Hash-based
.pycfiles (PEP 552) allow content-based validation instead of timestamp comparison, enabling reproducible bytecode in build systems. - Dict ordering is now a language guarantee, not just a CPython implementation detail. Code can rely on insertion order without worrying about interpreter portability.
- UTF-8 mode (
-X utf8orPYTHONUTF8=1) forces UTF-8 as the default encoding regardless of locale settings -- useful in containers and CI pipelines.
FAQ
Should I use dataclasses or attrs or plain classes in 2024?
dataclasses covers 80% of the use cases with zero dependencies. attrs is more powerful and older, with features like validators, converters, and slots support (though dataclasses also gained __slots__ support in 3.10). Choose dataclasses as the default; switch to attrs when you need validators or the performance of slots in older Python versions.
What is the difference between ContextVar and threading.local?
threading.local is per-thread. ContextVar is per-context -- and each coroutine automatically runs in its own child context. This means two coroutines on the same OS thread cannot see each other's ContextVar values unless they share a context explicitly. For async code, ContextVar is the correct tool.
Is the dict ordering guarantee retroactively true for Python 3.6?
In CPython 3.6, dict ordering was an implementation detail, not a spec guarantee. The guarantee was formalized in 3.7. Other Python implementations (PyPy, MicroPython) were not required to maintain insertion order before 3.7. In practice, if you need to support CPython only, 3.6 behaves the same, but portable code should target 3.7.
Does from __future__ import annotations affect runtime behavior?
Yes -- accessing __annotations__ directly will give you strings instead of resolved types. Code that does runtime annotation inspection (like Pydantic v1, older FastAPI, or custom DI frameworks) may break unless it calls typing.get_type_hints() to resolve the strings. Pydantic v2 handles this correctly.
When should I use asyncio.run() vs manually managing the event loop?
Use asyncio.run() for top-level entry points. It creates a fresh loop, runs the coroutine to completion, and cleans up. Only manage loops manually when embedding Python in a C extension or integrating with a foreign event loop (like a GUI toolkit's event loop). asyncio.run() should not be called from inside an already-running loop -- use await or asyncio.create_task() instead.