Overview
Python 3.6, released on December 23, 2016, is a major release that introduces some of modern Python's most beloved features. f-strings revolutionize string formatting, variable annotations extend the type system, and the secrets module provides a secure solution for token generation.
This version marks a turning point in Python 3 adoption: with such practical day-to-day improvements, many projects finally made the leap from Python 2. Async generators and underscores in numeric literals round out a remarkable set of new features.
Major Features
f-strings (PEP 498)
f-strings (formatted string literals) are arguably the most popular addition in Python 3.6. By prefixing a string with f, you can embed Python expressions directly inside curly braces. This is more concise, more readable, and faster than str.format() or %-style formatting.
# Basic f-string formatting
name = "Alice"
age = 32
print(f"My name is {name} and I am {age} years old.")
# My name is Alice and I am 32 years old.
# Expressions in f-strings
price_excl = 149.99
vat = 0.20
print(f"Price incl. VAT: {price_excl * (1 + vat):.2f} EUR")
# Price incl. VAT: 179.99 EUR
# Calling functions and methods
inventory = ["keyboard", "mouse", "monitor", "headset"]
print(f"Items in stock: {len(inventory)}")
# Items in stock: 4
print(f"First item: {inventory[0].upper()}")
# First item: KEYBOARD
# Comparison with older methods
city = "Lyon"
population = 516092
# Old style (%)
print("%s: %d inhabitants" % (city, population))
# .format() style
print("{}: {} inhabitants".format(city, population))
# f-string (most readable)
print(f"{city}: {population:,} inhabitants")
# Lyon: 516,092 inhabitants
# Nested f-strings
width = 12
value = 3.14159
print(f"{f'{value:.4f}':>{width}}")
# 3.1416
# Dictionaries in f-strings
server = {"host": "192.168.1.10", "port": 8080}
print(f"Connecting to {server['host']}:{server['port']}")
# Connecting to 192.168.1.10:8080
Variable annotations (PEP 526)
Python 3.6 introduces a syntax for annotating variable types, complementing the function annotations already available. These annotations have no runtime effect, but they improve code documentation and allow static analysis tools like mypy to detect type errors.
# Simple variable annotations
name: str = "Inspyration"
version: int = 3
active: bool = True
ratio: float = 0.85
# Annotations with complex types
from typing import List, Dict, Optional
extensions: List[str] = [".py", ".pyx", ".pyi"]
counters: Dict[str, int] = {"visits": 0, "errors": 0}
result: Optional[str] = None
# Annotated class variables
class Project:
name: str
version: str
dependencies: List[str]
def __init__(self, name: str, version: str) -> None:
self.name = name
self.version = version
self.dependencies = []
# Practical typed configuration class
class DBConfig:
host: str = "localhost"
port: int = 5432
db_name: str = "app_db"
pool_min: int = 2
pool_max: int = 10
ssl_enabled: bool = False
config = DBConfig()
print(f"Connecting to {config.host}:{config.port}/{config.db_name}")
# Connecting to localhost:5432/app_db
# Annotations are accessible via __annotations__
print(DBConfig.__annotations__)
# {'host': <class 'str'>, 'port': <class 'int'>, ...}
The secrets module
The secrets module provides functions for generating cryptographically secure random data. Unlike the random module, which uses a pseudo-random generator (predictable if the seed is known), secrets uses the operating system's entropy sources and is suitable for generating passwords, authentication tokens, and secure URLs.
import secrets
import string
# Generate a hex token (ideal for API keys)
hex_token = secrets.token_hex(32)
print(f"API key: {hex_token}")
# API key: a3f1b9c8d4e2... (64 hex characters)
# Generate a URL-safe token (ideal for password reset links)
url_token = secrets.token_urlsafe(48)
print(f"https://example.com/reset?token={url_token}")
# Generate a strong password
def generate_password(length=16):
"""Generate a secure password with uppercase, lowercase,
digits, and special characters."""
alphabet = string.ascii_letters + string.digits + string.punctuation
# Ensure at least one character from each category
while True:
pwd = ''.join(secrets.choice(alphabet) for _ in range(length))
if (any(c.islower() for c in pwd)
and any(c.isupper() for c in pwd)
and any(c.isdigit() for c in pwd)
and any(c in string.punctuation for c in pwd)):
return pwd
print(f"Password: {generate_password()}")
# Password: k9#Lm2$xPq7&vN!w
# Difference with random (DO NOT use random for security)
import random
# random.choice(alphabet) # Pseudo-random, predictable!
# secrets.choice(alphabet) # Cryptographically secure
# Secure token comparison (resistant to timing attacks)
stored_token = secrets.token_hex(16)
received_token = stored_token # Simulate a valid token
print(secrets.compare_digest(stored_token, received_token)) # True
Underscores in numeric literals
Python 3.6 allows using the underscore character (_) as a visual separator in numeric literals. This greatly improves the readability of large numbers, hexadecimal values, binary values, and octal values.
# Large decimal numbers
world_population = 7_900_000_000
annual_budget = 1_250_000.50
print(f"Population: {world_population:_}") # 7_900_000_000
print(f"Budget: {annual_budget:,.2f} EUR") # 1,250,000.50 EUR
# Hexadecimal values (colors, memory addresses)
bg_color = 0xFF_CC_00 # Yellow
netmask = 0xFF_FF_FF_00
print(f"Color: #{bg_color:06X}") # Color: #FFCC00
# Binary values (bit masks, registers)
permissions = 0b_111_101_101 # rwxr-xr-x (755)
register = 0b_1010_0011_1100_0101
print(f"Permissions: {permissions:o}") # 755
# Octal values
file_perms = 0o_755
dir_perms = 0o_750
# Readability comparison
without_separator = 10000000000
with_separator = 10_000_000_000
# The second is clearly 10 billion
Async generators (PEP 525)
Python 3.6 allows using yield inside async def functions, creating asynchronous generators. This fills an important gap: you can now produce values one at a time asynchronously, which is ideal for data streaming, API pagination, or reading continuous data feeds.
import asyncio
# Async generator: produces data in batches
async def sensor_stream(name, num_readings=5, delay=0.1):
"""Simulate an IoT sensor data stream."""
import random
for i in range(num_readings):
await asyncio.sleep(delay) # Simulate sensor wait
temperature = 20.0 + random.uniform(-3, 3)
yield {"sensor": name, "reading": i + 1, "temp": round(temperature, 1)}
# Consuming with async for
async def monitor_sensors():
"""Read data from multiple sensors."""
async for data in sensor_stream("server-room", num_readings=3):
print(f"[{data['sensor']}] Reading {data['reading']}: "
f"{data['temp']}°C")
# asyncio.run(monitor_sensors())
# [server-room] Reading 1: 21.3°C
# [server-room] Reading 2: 18.7°C
# [server-room] Reading 3: 20.1°C
# Async generator for API pagination
async def paginate_results(base_url, page_size=10):
"""Walk through API pages asynchronously."""
page = 1
while True:
# Simulate an API call
await asyncio.sleep(0.05)
items = [f"item_{(page - 1) * page_size + i}"
for i in range(page_size)]
if not items or page > 3: # Limit for the example
break
yield items
page += 1
async def process_all_pages():
async for batch in paginate_results("https://api.example.com/items"):
print(f"Received {len(batch)} items: {batch[0]} ... {batch[-1]}")
# asyncio.run(process_all_pages())
Minor Improvements
- Dictionaries now preserve insertion order (CPython implementation detail, guaranteed from Python 3.7 onwards).
- New implementation of the string formatting system with
str.__format__. - The
asynciomodule is stabilized and no longer provisional. - Addition of
os.fspath()and the__fspath__protocol (PEP 519) for better file path integration. - The
typingmodule gainsNamedTuplewith class syntax. - Async comprehensions and
awaitexpressions in comprehensions are supported (PEP 530).
Deprecations and Removals
Notable changes:
- The default filesystem encoding on Windows switches to UTF-8 (PEP 529).
- The
asynchatmodule is marked as deprecated. - The
asyncoremodule is marked as deprecated. - The
ssl.PROTOCOL_SSLv23constant is renamed tossl.PROTOCOL_TLS.
