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:

ServicePython 3.5Python 3.6Improvement
API Gateway450 req/s520 req/s15%
User Service380 req/s425 req/s12%
Email Service290 req/s310 req/s7%

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:

  1. F-strings are amazing - use them everywhere
  2. Type hints catch bugs early - use mypy
  3. Async improvements make concurrent code easier
  4. Migration from 3.5 is painless
  5. 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.