Contents
Appendix
  1. 34.Appendix A — Moving old Python code to the modern style
34 Chapter

Appendix A — Moving old Python code to the modern style

A step-by-step guide for moving 2017-era Python code (% strings, has_key, type() comparison, etc.) to modern Python. So readers of the old 21-part Python basics tutorial can continue naturally into this book.

This appendix is a migration guide for people who read this site’s old 21-part Python basics tutorial (2017), or who learned Python in the 2.x ~ 3.5 era and then stopped there.

If Chapters 1–33 are “starting modern from the beginning,” this appendix is a guide for taking old code in hand and moving it to modern one step at a time. Knowing both lets you keep the old tutorial’s material and refresh it instead of throwing it away.

Old / new 1:1 mapping — a one-page summary #

The table below is the compressed form of this appendix. Each row is revisited with paired code further down.

AreaOld code (~2017)New code (modern)
Outputprint "hello" (py2)print("hello")
String formatting"hi %s" % name / .format()f"hi {name}"
Integer division5 / 22 (py2)5 / 22.5 / integer is 5 // 2
dict key checkd.has_key("k")"k" in d
Exception catchexcept Exception, e:except Exception as e:
Iteration rangexrange(10)range(10) (already lazy)
Unicodeu"한글", unicode typejust "한글", every str is unicode
Type checktype(x) == intisinstance(x, int)
Class boilerplatedef __init__(self, a, b): self.a=a; self.b=b@dataclass
Type info:type x: int in docstringx: int annotation
Collection typesfrom typing import List + List[int]list[int] (3.9+)
Optional typeOptional[int]`int
Pathsos.path.join(a, b)Path(a) / b
External commandsos.system("ls") / Popensubprocess.run(["ls"])
Asynccallbacks / generator coroutinesasync def + await
Toolchainpip install + venv + pyenvuv add + uv run + uv python install

Step-by-step conversions #

1. print became a function #

Old (Python 2)
print "hello"
print "x =", x
New (Python 3)
print("hello")
print("x =", x)

Run old code through a Python 3 interpreter and you get a SyntaxError immediately. It’s a mechanical conversion, and tools catch it in one shot (see §“Automation” below).

2. String formatting — % / .format() → f-string #

Three generations coexist. New code is f-string, no exceptions.

Old (% style)
"hi %s, you are %d" % (name, age)
Middle (.format)
"hi {}, you are {}".format(name, age)
"hi {name}, you are {age}".format(name=name, age=age)
New (f-string, 3.6+)
f"hi {name}, you are {age}"
f"{name = }, {age = }"   # for debugging

For detailed usage, see Chapter 2 Variables, basic types, and type hints.

3. Integer division #

Old (Python 2)
5 / 2     # 2  (int / int returns int)
5.0 / 2   # 2.5
New (Python 3)
5 / 2     # 2.5  (always float)
5 // 2    # 2    (explicit integer division with //)

If old code wrote / and expected an int, it breaks in Python 3. If you meant integer division, write // explicitly.

4. dict methods — has_key is gone #

Old
if d.has_key("user"):
    ...
New
if "user" in d:
    ...

in is shorter and works consistently across non-dict collections.

5. Exception catch — , eas e #

Old (Python 2)
try:
    do_work()
except Exception, e:
    print "failed:", e
New (Python 3)
try:
    do_work()
except Exception as e:
    print(f"failed: {e}")

The , syntax is a SyntaxError in Python 3. Standardize on as.

Additionally, from Python 3.11 onwards there’s ExceptionGroup + except*. It’s a new tool for handling multiple simultaneously raised exceptions, covered in Chapter 6.

6. xrange is gone — range is already lazy #

Old (Python 2)
for i in xrange(10):
    ...
New (Python 3)
for i in range(10):
    ...

Python 3’s range absorbed the behavior of old xrange. It doesn’t materialize a list up front — it yields values lazily.

7. Unicode — every str is unicode #

Old (Python 2)
s = u"한글"     # needed the u prefix
isinstance(s, unicode)
New (Python 3)
s = "한글"      # plain str is already unicode
isinstance(s, str)

The unicode type doesn’t exist. str = unicode, bytes = byte sequence. The two don’t auto-convert; use .encode() / .decode() explicitly.

8. Type checks — use isinstance() instead of type() comparison #

Old
if type(x) == int:
    ...
New
if isinstance(x, int):
    ...

isinstance understands inheritance. type() comparison is true only for the exact same type, so it doesn’t handle subclasses. And in modern Python, the trend is to move away from runtime type checks altogether and rely on type hints + static verification (see Chapter 2 type checkers).

9. Class boilerplate — dozens of __init__ lines compressed into one @dataclass #

Old
class User(object):
    def __init__(self, id, name, email):
        self.id = id
        self.name = name
        self.email = email

    def __repr__(self):
        return "User(id=%r, name=%r, email=%r)" % (self.id, self.name, self.email)

    def __eq__(self, other):
        if not isinstance(other, User):
            return False
        return (self.id, self.name, self.email) == (other.id, other.name, other.email)
New (dataclass, 3.7+)
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str

__init__, __repr__, and __eq__ are generated automatically. Extra options (frozen, slots, kw_only) are all available in the same one-liner. Covered in depth in Chapter 8 dataclass and __slots__.

The (object) in class User(object): is another old artifact. In Python 3 every class inherits from object automatically, so it’s omitted.

10. Type info — from docstring to annotation #

Old
def add(a, b):
    """
    :param a: first integer
    :type a: int
    :param b: second integer
    :type b: int
    :rtype: int
    """
    return a + b
New
def add(a: int, b: int) -> int:
    """Add the two and return the result."""
    return a + b

Type info moves up to the signature. The IDE, the type checker, and the documentation generator all read the same source. The docstring is for why and how to use.

11. Collection types — typing import → built-in generics #

Old (3.8 and below)
from typing import List, Dict, Tuple, Set

names: List[str] = []
ages: Dict[str, int] = {}
point: Tuple[float, float] = (0.0, 0.0)
unique: Set[str] = set()
New (3.9+)
names: list[str] = []
ages: dict[str, int] = {}
point: tuple[float, float] = (0.0, 0.0)
unique: set[str] = set()

from typing import List itself is going away. You write [] directly on built-in types.

12. Optional types — OptionalT | None #

Old (3.9 and below)
from typing import Optional, Union

def find(id: int) -> Optional[str]:
    ...

def parse(s: str) -> Union[int, float, None]:
    ...
New (3.10+)
def find(id: int) -> str | None:
    ...

def parse(s: str) -> int | float | None:
    ...

Shorter, with no extra import.

13. Paths — os.pathpathlib.Path #

Old
import os
config_path = os.path.join(os.path.dirname(__file__), "config", "app.toml")
with open(config_path, "r") as f:
    data = f.read()
New
from pathlib import Path
config_path = Path(__file__).parent / "config" / "app.toml"
data = config_path.read_text()

Path objects compose with the / operator and carry methods like .read_text() / .write_text() / .exists() / .glob(). You memorize fewer os.path functions.

14. External commands — os.system / Popensubprocess.run #

Old
import os
os.system("ls -la /tmp")          # no result captured, security risk
Middle (manual Popen)
from subprocess import Popen, PIPE
proc = Popen(["ls", "-la", "/tmp"], stdout=PIPE, stderr=PIPE)
out, err = proc.communicate()
New (3.5+)
import subprocess
result = subprocess.run(
    ["ls", "-la", "/tmp"],
    capture_output=True,
    text=True,
    check=True,
)
print(result.stdout)

Passing args as a list is the default — less shell-injection risk. check=True raises on non-zero exit, capture_output=True captures stdout / stderr, text=True returns str instead of bytes.

15. Async — callbacks / generator coroutines → async def + await #

Old (asyncio's @coroutine + yield from, 3.4 ~ 3.7)
import asyncio

@asyncio.coroutine
def fetch():
    response = yield from get_url("https://example.com")
    return response
New (3.5+, recommended since 3.8)
async def fetch():
    response = await get_url("https://example.com")
    return response

The async def / await keywords became standard, and the @asyncio.coroutine decorator is deprecated. Covered in Chapter 14 async intro and Chapter 18 async in depth.

Automation — you don’t have to do all of this by hand #

Most of the conversions above are mechanical. Tools catch them in one pass.

pyupgrade #

run pyupgrade
uv tool install pyupgrade
pyupgrade --py312-plus path/to/file.py

What it handles (partial list):

  • % format → f-string (simple cases)
  • Drop from typing import List + replace List[int] with list[int]
  • Optional[X]X | None
  • Dict[K, V]dict[K, V]
  • super(ClassName, self)super()
  • Old print statements → function form
  • .format() cleanup inside f-strings

The --py312-plus argument means “assume my code only runs on Python 3.12+ and rewrite everything to the newest syntax available there.” For this book’s baseline, that’s --py314-plus.

ruff’s UP rules — pyupgrade integrated into ruff #

pyproject.toml
[tool.ruff.lint]
select = ["UP"]    # enable pyupgrade rules
auto-fix with ruff
uv run ruff check . --select UP --fix

ruff absorbed most of pyupgrade’s rules and you can run them alongside other lint rules in one pass. Wire it into pre-commit and old patterns are blocked from coming back — Chapter 30 Type checker setup and CI integration covers the formal setup.

What can’t be automated #

  • Converting a class to @dataclass (intent judgment needed)
  • Converting os.path to pathlib (depends on where and how it’s used; human judgment)
  • Callbacks / generator coroutines → async def (structural change)
  • Docstring type notation → annotations (tools exist, accuracy is limited)

The four above are safer to migrate by hand.

Old tutorial → this book — a recommended path #

If you came from the old 21-part Python basics tutorial, I recommend this order.

  1. This Appendix A first — see at a glance how patterns from the old tutorial era look different in the new book.
  2. Chapter 1 Getting started and uv setup — the toolchain changed completely, so reinstall the environment from scratch.
  3. Chapter 2 Variables, basic types, and type hints — internalize the biggest shift, “type hints first.”
  4. Chapters 3–7 — revisit topics you already know from the old tutorial (control flow, collections, functions, exceptions, modules) in modern phrasing. Familiarity makes them quick reads.
  5. Part 2 onward — enter areas the old tutorial didn’t cover (dataclass, Protocol, async, FastAPI, operations).

The old tutorial remains permanently on the site. Even after you’ve moved on to this book, occasionally crack open an old page and compare “this phrasing got rewritten this way in the new book.”

Exercises #

Move an entire chunk of old code to modern style at once.

Old code — old_user.py
import os

class User(object):
    def __init__(self, id, name, email):
        self.id = id
        self.name = name
        self.email = email

    def __repr__(self):
        return "User(id=%r, name=%r)" % (self.id, self.name)

def load_users(path):
    """
    :param path: path to users.txt
    :type path: str
    :rtype: List[User]
    """
    full = os.path.join(os.path.dirname(__file__), path)
    users = []
    f = open(full, "r")
    try:
        for line in f:
            parts = line.strip().split(",")
            if len(parts) != 3:
                continue
            id_str, name, email = parts
            users.append(User(int(id_str), name, email))
    finally:
        f.close()
    return users
  1. Convert the code above to modern style using this appendix’s mapping. Hints: @dataclass, type annotations, pathlib.Path, with open(...) as f:, list[User], f-string.
  2. Run pyright on the converted code and confirm it’s error-free.
  3. Run pyupgrade --py314-plus old_user.py and compare which parts the tool handled automatically vs. which still required a human hand.

In one line: Most conversions of old Python code are mechanical, so pyupgrade and ruff --select UP catch them. The parts that need a human hand are @dataclass, pathlib, async/await, and docstring types. The recommended path from the old tutorial to this book is Appendix A → Chapter 1 → Chapter 2 → quick read through Chapters 3–7 → Part 2 onward.

X