New in Python3#

The source code can be found on py3.py.

Type Parameter Syntax#

New in Python 3.12

  • PEP 695 - Type Parameter Syntax

Python 3.12 introduces a cleaner, more intuitive syntax for defining generic classes and functions. Instead of importing TypeVar and Generic from the typing module, you can now use the [] bracket notation directly in class and function definitions. This makes generic code more readable and reduces boilerplate significantly.

# Old way (before Python 3.12)
from typing import TypeVar, Generic
T = TypeVar('T')
class Box(Generic[T]):
    def __init__(self, item: T) -> None:
        self.item = item

# New way (Python 3.12+)
class Box[T]:
    def __init__(self, item: T) -> None:
        self.item = item

# Generic functions
def first[T](items: list[T]) -> T:
    return items[0]

f-string Improvements#

New in Python 3.12

  • PEP 701 - Syntactic formalization of f-strings

F-strings have been significantly improved in Python 3.12. They now support nested quotes of the same type, backslash escapes, and multi-line expressions without any limitations. This makes f-strings much more flexible and eliminates many edge cases that previously required workarounds.

>>> songs = ["Take me back to Eden", "&", "Satellite"]
>>> f"This is the playlist: {", ".join(songs)}"
'This is the playlist: Take me back to Eden, &, Satellite'

# Nested quotes now work
>>> f"He said {"hello"}"
'He said hello'

Exception Groups#

New in Python 3.11

  • PEP 654 - Exception Groups and except*

Exception groups allow you to raise and handle multiple unrelated exceptions simultaneously. This is particularly useful for concurrent operations where multiple tasks might fail independently. The new except* syntax lets you handle specific exception types from a group while letting others propagate.

>>> def raise_multiple():
...     raise ExceptionGroup("multiple errors", [
...         ValueError("invalid value"),
...         TypeError("wrong type"),
...     ])
...
>>> try:
...     raise_multiple()
... except* ValueError as e:
...     print(f"ValueError: {e.exceptions}")
... except* TypeError as e:
...     print(f"TypeError: {e.exceptions}")
...
ValueError: (ValueError('invalid value'),)
TypeError: (TypeError('wrong type'),)

Structural Pattern Matching#

New in Python 3.10

  • PEP 634 - Structural Pattern Matching: Specification

  • PEP 635 - Structural Pattern Matching: Motivation and Rationale

Pattern matching provides a powerful way to destructure and match complex data structures. It’s similar to switch statements in other languages but far more expressive, supporting sequence patterns, mapping patterns, class patterns, and guards. The wildcard _ matches anything and serves as a default case.

>>> def http_status(status):
...     match status:
...         case 200:
...             return "OK"
...         case 404:
...             return "Not Found"
...         case 500:
...             return "Internal Server Error"
...         case _:
...             return "Unknown"
...
>>> http_status(200)
'OK'
>>> http_status(404)
'Not Found'

# Pattern matching with destructuring
>>> def describe_point(point):
...     match point:
...         case (0, 0):
...             return "Origin"
...         case (x, 0):
...             return f"On x-axis at {x}"
...         case (0, y):
...             return f"On y-axis at {y}"
...         case (x, y):
...             return f"Point at ({x}, {y})"
...
>>> describe_point((0, 0))
'Origin'
>>> describe_point((5, 0))
'On x-axis at 5'

Dictionary Merge#

New in Python 3.9

  • PEP 584 - Add Union Operators To dict

The | operator provides a cleaner, more intuitive way to merge dictionaries. The |= operator updates a dictionary in place. This is more readable than using {**a, **b} or dict.update() and follows the pattern of set operations.

>>> a = {"foo": "Foo"}
>>> b = {"bar": "Bar"}

# old way
>>> {**a, **b}
{'foo': 'Foo', 'bar': 'Bar'}
>>> a.update(b)
>>> a
{'foo': 'Foo', 'bar': 'Bar'}

# new way
>>> a = {"foo": "Foo"}
>>> a | b
{'foo': 'Foo', 'bar': 'Bar'}
>>> a |= b
>>> a
{'foo': 'Foo', 'bar': 'Bar'}

Positional-only parameters#

New in Python 3.8

  • PEP 570 - Python Positional-Only Parameters

Parameters before the / marker must be passed positionally and cannot be used as keyword arguments. This gives library authors more flexibility in API design and allows parameter names to be changed without breaking backward compatibility.

>>> def f(a, b, /, c, d):
...     print(a, b, c, d)
...
>>> f(1, 2, 3, 4)
1 2 3 4
>>> f(1, 2, c=3, d=4)
1 2 3 4
>>> f(1, b=2, c=3, d=4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got some positional-only arguments passed as keyword arguments: 'b'

The walrus operator#

New in Python 3.8

  • PEP 572 - Assignment Expressions

The walrus operator := allows you to assign values to variables as part of an expression. This reduces code duplication when you need to both compute a value and use it in a condition. After completing PEP 572, Guido van Rossum, commonly known as BDFL, decided to resign as Python’s dictator.

>>> f = (0, 1)
>>> [(f := (f[1], sum(f)))[0] for i in range(10)]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

# Useful in while loops
>>> while (line := input("Enter: ")) != "quit":
...     print(f"You entered: {line}")

# Useful in if statements
>>> if (n := len("hello")) > 3:
...     print(f"Length {n} is greater than 3")

Data Classes#

New in Python 3.7

  • PEP 557 - Data Classes

Dataclasses automatically generate boilerplate code like __init__, __repr__, __eq__, and optionally __hash__ for classes that primarily store data. This reduces repetitive code and makes data-holding classes more concise and readable.

Mutable Data Class

>>> from dataclasses import dataclass
>>> @dataclass
... class DCls(object):
...     x: str
...     y: str
...
>>> d = DCls("foo", "bar")
>>> d
DCls(x='foo', y='bar')
>>> d = DCls(x="foo", y="baz")
>>> d
DCls(x='foo', y='baz')
>>> d.z = "bar"

Immutable Data Class

>>> from dataclasses import dataclass
>>> from dataclasses import FrozenInstanceError
>>> @dataclass(frozen=True)
... class DCls(object):
...     x: str
...     y: str
...
>>> try:
...     d.x = "baz"
... except FrozenInstanceError as e:
...     print(e)
...
cannot assign to field 'x'
>>> try:
...     d.z = "baz"
... except FrozenInstanceError as e:
...     print(e)
...
cannot assign to field 'z'

Built-in breakpoint()#

New in Python 3.7

  • PEP 553 - Built-in breakpoint()

The breakpoint() function provides a convenient way to drop into the debugger. It respects the PYTHONBREAKPOINT environment variable, allowing you to customize or disable debugging behavior without modifying code.

>>> for x in range(3):
...     print(x)
...     breakpoint()
...
0
> <stdin>(1)<module>()->None
(Pdb) c
1
> <stdin>(1)<module>()->None
(Pdb) c
2
> <stdin>(1)<module>()->None
(Pdb) c

Core support for typing module and generic types#

New in Python 3.7

  • PEP 560 - Core support for typing module and generic types

Python 3.7 added core support for the typing module, making generic types faster and enabling classes to customize how they’re subscripted via __class_getitem__.

Before Python 3.7

>>> from typing import Generic, TypeVar
>>> from typing import Iterable
>>> T = TypeVar('T')
>>> class C(Generic[T]): ...
...
>>> def func(l: Iterable[C[int]]) -> None:
...     for i in l:
...         print(i)
...
>>> func([1,2,3])
1
2
3

Python 3.7 or above

>>> from typing import Iterable
>>> class C:
...     def __class_getitem__(cls, item):
...         return f"{cls.__name__}[{item.__name__}]"
...
>>> def func(l: Iterable[C[int]]) -> None:
...     for i in l:
...         print(i)
...
>>> func([1,2,3])
1
2
3

Variable annotations#

New in Python 3.6

  • PEP 526 - Syntax for Variable Annotations

Variables can now be annotated with types using the : syntax, even without immediate assignment. This enables better static analysis and IDE support.

>>> from typing import List
>>> x: List[int] = [1, 2, 3]
>>> x
[1, 2, 3]

>>> from typing import List, Dict
>>> class Cls(object):
...     x: List[int] = [1, 2, 3]
...     y: Dict[str, str] = {"foo": "bar"}
...
>>> o = Cls()
>>> o.x
[1, 2, 3]
>>> o.y
{'foo': 'bar'}

f-string#

New in Python 3.6

  • PEP 498 - Literal String Interpolation

F-strings (formatted string literals) provide a concise and readable way to embed expressions inside string literals. They are faster than % formatting and str.format() because they are evaluated at runtime.

>>> py = "Python3"
>>> f'Awesome {py}'
'Awesome Python3'
>>> x = [1, 2, 3, 4, 5]
>>> f'{x}'
'[1, 2, 3, 4, 5]'
>>> def foo(x:int) -> int:
...     return x + 1
...
>>> f'{foo(0)}'
'1'
>>> f'{123.567:1.3}'
'1.24e+02'

Asynchronous generators#

New in Python 3.6

  • PEP 525 - Asynchronous Generators

Asynchronous generators combine the power of generators with async/await syntax, allowing you to yield values asynchronously. This is useful for streaming data from async sources.

>>> import asyncio
>>> async def fib(n: int):
...     a, b = 0, 1
...     for _ in range(n):
...         await asyncio.sleep(1)
...         yield a
...         b, a = a + b , b
...
>>> async def coro(n: int):
...     ag = fib(n)
...     f = await ag.asend(None)
...     print(f)
...     f = await ag.asend(None)
...     print(f)
...
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(coro(5))
0
1

Asynchronous comprehensions#

New in Python 3.6

  • PEP 530 - Asynchronous Comprehensions

Async comprehensions allow using async for in list, set, dict comprehensions and generator expressions. You can also use await expressions within comprehensions.

>>> import asyncio
>>> async def fib(n: int):
...     a, b = 0, 1
...     for _ in range(n):
...         await asyncio.sleep(1)
...         yield a
...         b, a = a + b , b
...

# async for ... else

>>> async def coro(n: int):
...     async for f in fib(n):
...         print(f, end=" ")
...     else:
...         print()
...
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(coro(5))
0 1 1 2 3

# async for in list

>>> async def coro(n: int):
...     return [f async for f in fib(n)]
...
>>> loop.run_until_complete(coro(5))
[0, 1, 1, 2, 3]

# await in list

>>> async def slowfmt(n: int) -> str:
...     await asyncio.sleep(0.5)
...     return f'{n}'
...
>>> async def coro(n: int):
...     return [await slowfmt(f) async for f in fib(n)]
...
>>> loop.run_until_complete(coro(5))
['0', '1', '1', '2', '3']

New dict implementation#

New in Python 3.6

  • PEP 468 - Preserving the order of **kwargs in a function

  • PEP 520 - Preserving Class Attribute Definition Order

  • bpo 27350 - More compact dictionaries with faster iteration

Python 3.6 introduced a new dictionary implementation that uses 20-25% less memory and preserves insertion order. This was an implementation detail in 3.6 but became a language guarantee in Python 3.7.

Before Python 3.5

>>> import sys
>>> sys.getsizeof({str(i):i for i in range(1000)})
49248

>>> d = {'timmy': 'red', 'barry': 'green', 'guido': 'blue'}
>>> d   # without order-preserving
{'barry': 'green', 'timmy': 'red', 'guido': 'blue'}

Python 3.6

  • Memory usage is smaller than Python 3.5

  • Preserve insertion ordered

>>> import sys
>>> sys.getsizeof({str(i):i for i in range(1000)})
36968

>>> d = {'timmy': 'red', 'barry': 'green', 'guido': 'blue'}
>>> d   # preserve insertion ordered
{'timmy': 'red', 'barry': 'green', 'guido': 'blue'}

async and await syntax#

New in Python 3.5

  • PEP 492 - Coroutines with async and await syntax

The async and await keywords provide native syntax for writing coroutines, making asynchronous code much more readable than the previous generator-based approach. This is the foundation of modern Python async programming.

Before Python 3.5

>>> import asyncio
>>> @asyncio.coroutine
... def fib(n: int):
...     a, b = 0, 1
...     for _ in range(n):
...         b, a = a + b, b
...     return a
...
>>> @asyncio.coroutine
... def coro(n: int):
...     for x in range(n):
...         yield from asyncio.sleep(1)
...         f = yield from fib(x)
...         print(f)
...
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(coro(3))
0
1
1

Python 3.5 or above

>>> import asyncio
>>> async def fib(n: int):
...     a, b = 0, 1
...     for _ in range(n):
...         b, a = a + b, b
...     return a
...
>>> async def coro(n: int):
...     for x in range(n):
...         await asyncio.sleep(1)
...         f = await fib(x)
...         print(f)
...
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(coro(3))
0
1
1

General unpacking#

New in Python 3.5

  • PEP 448 - Additional Unpacking Generalizations

Python 3.5 extended the * and ** unpacking operators to work in more contexts, including function calls with multiple unpacking operations and in list/dict literals.

Python 2

>>> def func(*a, **k):
...     print(a)
...     print(k)
...
>>> func(*[1,2,3,4,5], **{"foo": "bar"})
(1, 2, 3, 4, 5)
{'foo': 'bar'}

Python 3

>>> print(*[1, 2, 3], 4, *[5, 6])
1 2 3 4 5 6
>>> [*range(4), 4]
[0, 1, 2, 3, 4]
>>> {"foo": "Foo", "bar": "Bar", **{"baz": "baz"}}
{'foo': 'Foo', 'bar': 'Bar', 'baz': 'baz'}
>>> def func(*a, **k):
...     print(a)
...     print(k)
...
>>> func(*[1], *[4,5], **{"foo": "FOO"}, **{"bar": "BAR"})
(1, 4, 5)
{'foo': 'FOO', 'bar': 'BAR'}

Matrix multiplication#

New in Python 3.5

  • PEP 465 - A dedicated infix operator for matrix multiplication

The @ operator was added for matrix multiplication, primarily benefiting scientific computing libraries like NumPy. Classes can implement __matmul__ and __imatmul__ to support this operator.

>>> # "@" represent matrix multiplication
>>> class Arr:
...     def __init__(self, *arg):
...         self._arr = arg
...     def __matmul__(self, other):
...         if not isinstance(other, Arr):
...             raise TypeError
...         if len(self) != len(other):
...             raise ValueError
...         return sum([x*y for x, y in zip(self._arr, other._arr)])
...     def __imatmul__(self, other):
...         if not isinstance(other, Arr):
...             raise TypeError
...         if len(self) != len(other):
...             raise ValueError
...         res = sum([x*y for x, y in zip(self._arr, other._arr)])
...         self._arr = [res]
...         return self
...     def __len__(self):
...         return len(self._arr)
...     def __str__(self):
...         return self.__repr__()
...     def __repr__(self):
...         return "Arr({})".format(repr(self._arr))
...
>>> a = Arr(9, 5, 2, 7)
>>> b = Arr(5, 5, 6, 6)
>>> a @ b  # __matmul__
124
>>> a @= b  # __imatmul__
>>> a
Arr([124])

Format byte string#

New in Python 3.5

  • PEP 461 - Adding % formatting to bytes and bytearray

The % formatting operator now works with bytes and bytearray objects, making it easier to work with binary protocols and formats.

>>> b'abc %b %b' % (b'foo', b'bar')
b'abc foo bar'
>>> b'%d %f' % (1, 3.14)
b'1 3.140000'
>>> class Cls(object):
...     def __repr__(self):
...         return "repr"
...     def __str__(self):
...         return "str"
...
'repr'
>>> b'%a' % Cls()
b'repr'

Suppressing exception#

New in Python 3.3

  • PEP 409 - Suppressing exception context

When re-raising exceptions, Python shows the chain of exceptions by default. Using raise ... from None suppresses the context, showing only the new exception.

Without raise Exception from None

>>> def func():
...     try:
...         1 / 0
...     except ZeroDivisionError:
...         raise ArithmeticError
...
>>> func()
Traceback (most recent call last):
  File "<stdin>", line 3, in func
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in func
ArithmeticError

With raise Exception from None

>>> def func():
...     try:
...         1 / 0
...     except ZeroDivisionError:
...         raise ArithmeticError from None
...
>>> func()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in func
ArithmeticError

# debug

>>> try:
...     func()
... except ArithmeticError as e:
...     print(e.__context__)
...
division by zero

Generator delegation#

New in Python 3.3

  • PEP 380 - Syntax for Delegating to a Subgenerator

The yield from expression allows a generator to delegate part of its operations to another generator. This simplifies writing generators that consume other generators.

>>> def fib(n: int):
...     a, b = 0, 1
...     for _ in range(n):
...         yield a
...         b, a = a + b, b
...
>>> def delegate(n: int):
...     yield from fib(n)
...
>>> list(delegate(10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

BDFL retirement#

New in Python 3.1

  • PEP 401 - BDFL Retirement

An April Fools’ joke PEP that added a humorous easter egg. When you import barry_as_FLUFL from __future__, the != operator is replaced with <>.

>>> from __future__ import barry_as_FLUFL
>>> 1 != 2
  File "<stdin>", line 1
    1 != 2
       ^
SyntaxError: with Barry as BDFL, use '<>' instead of '!='
>>> 1 <> 2
True

Function annotations#

New in Python 3.0

  • PEP 3107 - Function Annotations

  • PEP 484 - Type Hints

  • PEP 483 - The Theory of Type Hints

Function annotations allow attaching metadata to function parameters and return values. While Python doesn’t enforce these at runtime, they enable static type checking tools and better IDE support.

>>> import types
>>> generator = types.GeneratorType
>>> def fib(n: int) -> generator:
...     a, b = 0, 1
...     for _ in range(n):
...         yield a
...         b, a = a + b, b
...
>>> [f for f in fib(10)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Extended iterable unpacking#

New in Python 3.0

  • PEP 3132 - Extended Iterable Unpacking

The * operator in unpacking captures remaining items into a list. This works in assignments and for loops, making it easy to split sequences.

>>> a, *b, c = range(5)
>>> a, b, c
(0, [1, 2, 3], 4)
>>> for a, *b in [(1, 2, 3), (4, 5, 6, 7)]:
...     print(a, b)
...
1 [2, 3]
4 [5, 6, 7]

Keyword-Only Arguments#

New in Python 3.0

  • PEP 3102 - Keyword-Only Arguments

Parameters defined after * in a function signature must be passed as keyword arguments. This improves API clarity and prevents accidental positional usage.

>>> def f(a, b, *, kw):
...     print(a, b, kw)
...
>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 2 positional arguments but 3 were given
>>> f(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'kw'
>>> f(1, 2, kw=3)
1 2 3

New Super#

New in Python 3.0

  • PEP 3135 - New Super

Python 3 simplified the super() call by making it work without arguments in most cases. The interpreter automatically determines the class and instance.

Python 2

>>> class ParentCls(object):
...     def foo(self):
...         print "call parent"
...
>>> class ChildCls(ParentCls):
...     def foo(self):
...         super(ChildCls, self).foo()
...         print "call child"
...
>>> p = ParentCls()
>>> c = ChildCls()
>>> p.foo()
call parent
>>> c.foo()
call parent
call child

Python 3

>>> class ParentCls(object):
...     def foo(self):
...         print("call parent")
...
>>> class ChildCls(ParentCls):
...     def foo(self):
...         super().foo()
...         print("call child")
...
>>> p = ParentCls()
>>> c = ChildCls()
>>> p.foo()
call parent
>>> c.foo()
call parent
call child

Add nonlocal keyword#

New in Python 3.0

  • PEP 3104 - Access to Names in Outer Scopes

The nonlocal keyword allows assigning to variables in an enclosing (but non-global) scope. This is useful for closures that need to modify outer variables.

>>> def outf():
...     o = "out"
...     def inf():
...         nonlocal o
...         o = "change out"
...     inf()
...     print(o)
...
>>> outf()
change out

Not allow from module import * inside function#

New in Python 3.0

Star imports are now only allowed at module level. This prevents namespace pollution and makes code more predictable.

>>> def f():
...     from os import *
...
  File "<stdin>", line 1
SyntaxError: import * only allowed at module level

Remove <>#

New in Python 3.0

The <> operator (alternative to !=) was removed in Python 3 to simplify the language. Use != for inequality comparisons.

Python 2

>>> a = "Python2"
>>> a <> "Python3"
True

# equal to !=
>>> a != "Python3"
True

Python 3

>>> a = "Python3"
>>> a != "Python2"
True

String is unicode#

New in Python 3.0

  • PEP 3138 - String representation in Python 3000

  • PEP 3120 - Using UTF-8 as the default source encoding

  • PEP 3131 - Supporting Non-ASCII Identifiers

In Python 3, all strings are Unicode by default. The str type represents Unicode text, while bytes represents binary data. This eliminates many encoding-related bugs common in Python 2.

Python 2

>>> s = 'Café'  # byte string
>>> s
'Caf\xc3\xa9'
>>> type(s)
<type 'str'>
>>> u = u'Café' # unicode string
>>> u
u'Caf\xe9'
>>> type(u)
<type 'unicode'>
>>> len([_c for _c in 'Café'])
5

Python 3

>>> s = 'Café'
>>> s
'Café'
>>> type(s)
<class 'str'>
>>> s.encode('utf-8')
b'Caf\xc3\xa9'
>>> s.encode('utf-8').decode('utf-8')
'Café'
>>> len([_c for _c in 'Café'])
4

Division Operator#

New in Python 3.0

  • PEP 238 - Changing the Division Operator

In Python 3, the / operator always performs true division (returning a float), while // performs floor division. This eliminates a common source of bugs.

Python 2

>>> 1 / 2
0
>>> 1 // 2
0
>>> 1. / 2
0.5

# back port "true division" to python2

>>> from __future__ import division
>>> 1 / 2
0.5
>>> 1 // 2
0

Python 3

>>> 1 / 2
0.5
>>> 1 // 2
0