Python 3.12: F-String Improvements and GIL Steps

python dev

Python 3.12 was released in October 2023, continuing the momentum from 3.11’s performance gains. Here’s what matters.

F-String Improvements

Nested Expressions

You can now nest f-strings without escaping:

# Before 3.12 - had to use different quotes
name = "Alice"
result = f"User said: '{f\"Hello, {name}\"}'"  # Awkward

# Python 3.12 - reuse quotes freely
result = f"User said: "{f"Hello, {name}"}"    # Clean

Multi-line Expressions

# Python 3.12 - multi-line expressions in f-strings
query = f"""
SELECT *
FROM users
WHERE name = {
    user.name
    if user.name is not None
    else 'Anonymous'
}
"""

Comments Inside F-Strings

# Python 3.12
data = f"{
    x  # This is x
    + y  # Plus y
}"

Backslashes Allowed

# Python 3.12
escaped = f"Path: {path.replace("\\", "/")}"  # Now allowed

Better Error Messages

Python 3.12 continues improving error messaging:

# More specific errors
lst = [1, 2, 3]
lst[10]  # IndexError: list index out of range. The list has only 3 items.

# Better suggestions
import colections  
# Did you mean 'collections'?

Type System Enhancements

TypedDict With Defaults

from typing import TypedDict, NotRequired

class User(TypedDict):
    name: str
    email: str
    phone: NotRequired[str]  # Optional, won't error if missing

Override Decorator

from typing import override

class Parent:
    def process(self) -> int:
        return 0

class Child(Parent):
    @override  # Type checker verifies this actually overrides
    def process(self) -> int:
        return 1
    
    @override  # Error! 'handle' doesn't exist in parent
    def handle(self) -> int:
        return 2

Generic Type Alias Syntax

# Python 3.12
type Point = tuple[float, float]
type Handler[T] = Callable[[T], None]
type Matrix[T] = list[list[T]]

# Instead of
from typing import TypeAlias
Point: TypeAlias = tuple[float, float]

Per-Interpreter GIL (PEP 684)

The big architectural change:

# Previously: One GIL for entire Python process
# Python 3.12: Each interpreter can have its own GIL

import interpreters  # Experimental

interp = interpreters.create()
# This interpreter has its own GIL
# True parallelism possible across interpreters

Still experimental, but foundational for Python’s parallel future.

Memory Improvements

Smaller Object Sizes

# Objects use less memory in 3.12
import sys

d = {"a": 1, "b": 2, "c": 3}
sys.getsizeof(d)  # Smaller than 3.11

Immortal Objects

Certain objects never need reference counting:

# None, True, False are immortal in 3.12
# Reduces cache coherence overhead in multicore

Workflow Changes

pathlib Improvements

from pathlib import Path

# New walk method
for root, dirs, files in Path(".").walk():
    for file in files:
        print(root / file)
        
# New with_segments method
p = Path("/usr/local/bin")
p.with_segments("var", "log")  # Path('/var/log')

itertools Additions

from itertools import batched

# Split into batches
list(batched("ABCDEFG", 3))
# [('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)]

Improved sqlite3

import sqlite3

# Command-line tool improvements
python -m sqlite3 database.db

# Better type handling
conn = sqlite3.connect(":memory:", autocommit=True)  # New parameter

Deprecations

Deprecated

# asyncio.get_event_loop() without running loop → deprecated
# Use asyncio.get_running_loop() instead

# datetime.utcnow() and utcfromtimestamp() → deprecated
# Use datetime.now(UTC) instead

from datetime import datetime, timezone
now = datetime.now(timezone.utc)  # Correct way

Removed

# distutils is gone
# Use setuptools or build instead

# From setuptools
from setuptools import setup

Performance

Continued gains from 3.11:

Task3.103.113.12
Startup1.0x0.9x0.85x
Regular code1.0x1.25x1.30x
Frame creation1.0x2.0x2.2x

Improvements are cumulative—upgrade path matters.

Upgrading

Compatibility Check

python -Werror -c "import your_project"
# Run with deprecation warnings as errors

Common Issues

# Removed distutils
# Before
from distutils.core import setup
# After
from setuptools import setup

# datetime changes
# Before
from datetime import datetime
now = datetime.utcnow()
# After
from datetime import datetime, timezone
now = datetime.now(timezone.utc)

Test Your Code

# Run tests with Python 3.12
python3.12 -m pytest

# Check for deprecation warnings
python3.12 -W default your_script.py

Should You Upgrade?

Yes, if:

Wait, if:

Final Thoughts

Python 3.12 is a solid release. The f-string improvements alone make code cleaner. The per-interpreter GIL is the start of something bigger—true Python parallelism is on the roadmap.

Upgrade when your dependencies allow.


Python 3.12: Better syntax, better types, better future.

All posts