Python 3.6 in Production: F-Strings and Type Hints
Python 3.6 was released in December, and I immediately started testing it. After a month of experimentation, we’ve migrated 3 microservices to 3.6 in production.
The new features are game-changing. Here’s what I love about Python 3.6.
Table of Contents
F-Strings: Finally, Readable String Formatting
This is my favorite feature. F-strings make string formatting so much cleaner.
Before (Python 3.5):
# Old style formatting
name = "Alice"
age = 30
message = "Hello, %s. You are %d years old." % (name, age)
# str.format()
message = "Hello, {}. You are {} years old.".format(name, age)
message = "Hello, {name}. You are {age} years old.".format(name=name, age=age)
Python 3.6 f-strings:
name = "Alice"
age = 30
message = f"Hello, {name}. You are {age} years old."
So much cleaner! And you can use expressions:
# Expressions in f-strings
price = 19.99
quantity = 3
print(f"Total: ${price * quantity:.2f}") # Total: $59.97
# Method calls
user = User(name="Bob")
print(f"User: {user.name.upper()}") # User: BOB
# Dictionary access
data = {"status": "active"}
print(f"Status: {data['status']}") # Status: active
I’ve already refactored hundreds of lines to use f-strings. The code is much more readable.
Type Hints: Better Than Documentation
Python 3.5 introduced type hints, but 3.6 makes them more powerful with variable annotations.
Before:
def get_user(user_id):
"""Get user by ID.
Args:
user_id (int): The user ID
Returns:
dict: User data
"""
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
With type hints:
from typing import Dict, Optional
def get_user(user_id: int) -> Optional[Dict[str, any]]:
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
The type information is in the code, not just comments. IDEs can use this for autocomplete and error checking.
Variable annotations (new in 3.6):
# Python 3.5 - awkward
users = [] # type: List[User]
# Python 3.6 - clean
users: List[User] = []
# Class attributes
class UserService:
cache: Dict[int, User] = {}
timeout: int = 30
Using mypy for Type Checking
Type hints are optional at runtime, but you can check them with mypy:
pip install mypy
mypy app.py
Example:
def add(a: int, b: int) -> int:
return a + b
result = add(1, "2") # mypy error: Argument 2 has incompatible type "str"
I’ve integrated mypy into our CI pipeline:
# .gitlab-ci.yml
test:
script:
- pip install mypy
- mypy src/
- pytest
This catches type errors before they reach production.
Async Improvements
Python 3.6 improves async/await syntax. You can now use async generators and comprehensions:
# Async generator
async def fetch_pages(urls):
for url in urls:
response = await fetch(url)
yield response
# Async comprehension
results = [await fetch(url) for url in urls]
# Async for
async for response in fetch_pages(urls):
process(response)
Real-world example - fetching multiple APIs:
import asyncio
import aiohttp
async def fetch_user(session, user_id):
async with session.get(f'https://api.example.com/users/{user_id}') as response:
return await response.json()
async def fetch_all_users(user_ids):
async with aiohttp.ClientSession() as session:
tasks = [fetch_user(session, uid) for uid in user_ids]
return await asyncio.gather(*tasks)
# Fetch 100 users concurrently
users = asyncio.run(fetch_all_users(range(1, 101)))
This is much faster than sequential requests.
Secrets Module
Python 3.6 adds a secrets module for cryptographically strong random numbers:
import secrets
# Generate secure token
token = secrets.token_hex(16) # 32 character hex string
# Generate URL-safe token
token = secrets.token_urlsafe(32)
# Compare strings securely (prevents timing attacks)
if secrets.compare_digest(user_token, expected_token):
print("Valid token")
Before, we used os.urandom() or random.SystemRandom(). The secrets module is clearer and safer.
Underscores in Numeric Literals
For readability, you can now use underscores in numbers:
# Before
million = 1000000
billion = 1000000000
# Python 3.6
million = 1_000_000
billion = 1_000_000_000
# Works with hex, binary, octal
hex_value = 0xFF_FF_FF_FF
binary = 0b_1111_0000
This makes large numbers much easier to read.
Preserving Dictionary Order
In CPython 3.6, dictionaries preserve insertion order. This is an implementation detail in 3.6, but becomes guaranteed in 3.7.
user = {
'name': 'Alice',
'age': 30,
'email': 'alice@example.com'
}
# Iteration order matches insertion order
for key in user:
print(key) # name, age, email (in that order)
This simplifies code that relied on OrderedDict.
Migrating from Python 2.7
We still have some Python 2.7 services. Migration challenges:
1. Print function
# Python 2
print "Hello"
# Python 3
print("Hello")
2. Integer division
# Python 2
5 / 2 # Returns 2
# Python 3
5 / 2 # Returns 2.5
5 // 2 # Returns 2 (floor division)
3. Unicode strings
# Python 2
u"Hello" # Unicode string
"Hello" # Byte string
# Python 3
"Hello" # Unicode string (default)
b"Hello" # Byte string
4. Iterators
# Python 2
range(10) # Returns list
dict.keys() # Returns list
# Python 3
range(10) # Returns iterator
dict.keys() # Returns view object
I use 2to3 tool for automated conversion:
2to3 -w app.py
But manual review is still needed.
Real-World Migration Example
Here’s a Flask service before and after Python 3.6:
Before (Python 2.7):
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/users/<int:user_id>')
def get_user(user_id):
user = db.query("SELECT * FROM users WHERE id = %s" % user_id)
if user:
return jsonify({
'id': user['id'],
'name': user['name'],
'status': 'User {} is {}'.format(user['name'], user['status'])
})
return jsonify({'error': 'Not found'}), 404
After (Python 3.6):
from flask import Flask, jsonify
from typing import Dict, Tuple, Union
app = Flask(__name__)
@app.route('/users/<int:user_id>')
def get_user(user_id: int) -> Union[Dict, Tuple[Dict, int]]:
user: Optional[Dict] = db.query(f"SELECT * FROM users WHERE id = {user_id}")
if user:
return jsonify({
'id': user['id'],
'name': user['name'],
'status': f"User {user['name']} is {user['status']}"
})
return jsonify({'error': 'Not found'}), 404
Changes:
- F-strings for formatting
- Type hints for parameters and return values
- Variable annotations
Performance
Python 3.6 is faster than 3.5:
- Dictionaries: 20-25% smaller memory footprint
- Function calls: Faster due to new calling convention
- Async: Better performance for async/await
Benchmarks on our services:
| Service | Python 3.5 | Python 3.6 | Improvement |
|---|---|---|---|
| API Gateway | 450 req/s | 520 req/s | 15% |
| User Service | 380 req/s | 425 req/s | 12% |
| Email Service | 290 req/s | 310 req/s | 7% |
Not huge, but free performance is always welcome.
Deployment
We use Docker, so upgrading is easy:
# Before
FROM python:3.5-slim
# After
FROM python:3.6-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]
Rebuild and deploy:
docker build -t myservice:3.6 .
docker push myservice:3.6
kubectl set image deployment/myservice myservice=myservice:3.6
Challenges
1. Third-party libraries
Some libraries don’t support 3.6 yet. Check compatibility:
pip install caniusepython3
caniusepython3 -r requirements.txt
2. Breaking changes
Some code broke during migration:
# Python 2/3.5 - works
async def fetch():
return await get_data()
# Python 3.6 - 'await' is now a keyword, can't use as variable name
await = 5 # SyntaxError
3. Testing
We run tests on both 3.5 and 3.6 during migration:
# .gitlab-ci.yml
test-py35:
image: python:3.5
script:
- pip install -r requirements.txt
- pytest
test-py36:
image: python:3.6
script:
- pip install -r requirements.txt
- pytest
Should You Upgrade?
Yes, if:
- You’re on Python 3.5 (easy upgrade)
- You want better performance
- You love f-strings (you will)
- You use type hints
Wait, if:
- You’re on Python 2.7 (bigger migration)
- Your dependencies don’t support 3.6
- You’re risk-averse (wait for 3.6.1)
For us, the upgrade was worth it. F-strings alone justify the migration.
Future: Python 3.7
Python 3.7 is coming soon. Features I’m excited about:
- Data classes - Less boilerplate for simple classes
- Guaranteed dict order - No more OrderedDict
- Breakpoint() - Built-in debugger
- Faster - More performance improvements
I’ll upgrade as soon as it’s released.
Conclusion
Python 3.6 is a solid release. F-strings and type hints make code more readable and maintainable.
Key takeaways:
- F-strings are amazing - use them everywhere
- Type hints catch bugs early - use mypy
- Async improvements make concurrent code easier
- Migration from 3.5 is painless
- Performance improvements are a nice bonus
If you’re still on Python 2.7, start planning your migration. Python 2 EOL is in 2020.
If you’re on Python 3.5, upgrade to 3.6 now. You won’t regret it.
Python keeps getting better. I’m excited for the future.