Skip to content

Instantly share code, notes, and snippets.

@lukereding
Created June 23, 2021 10:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukereding/2dde6fee63b0c161d88600fed88df863 to your computer and use it in GitHub Desktop.
Save lukereding/2dde6fee63b0c161d88600fed88df863 to your computer and use it in GitHub Desktop.
New features in python 3.8 and 3.9

What's new in Python 3.8

The walrus operator

:= lets you assign variables as part of larger expressions.

Example:

a = list(range(100))

# without the walrus:
# this is fine, but we call `len(a)` twice
if len(a) > 10:
    print(f"List is too long ({len(a)} elements, expected <= 10)")
List is too long (100 elements, expected <= 10)
# with the walrus:
# we assign `n` within the `if` expression
# we only have to call `len(a)` once
if (n := len(a)) > 10:
    print(f"List is too long ({n} elements, expected <= 10)")
List is too long (100 elements, expected <= 10)

Potential use cases:

  1. In list comps where a value used in a filtering expression is also needed in the expression body. Example:
features = [f"feature_{i:06}" for i in list(range(1, 20))]

[feat_no for feature in features if (feat_no := int(feature.removeprefix("feature_"))) % 3 == 0]
[3, 6, 9, 12, 15, 18]
from random import random
def f(x):
    return x + random()

data = list(range(5))
[y for x in data if (y := f(x)) > 3]
[3.1250742782596097, 4.640482486271297]
  1. Resist calling a function more than once if it's expensive to compute
from time import sleep, time

def f():
    sleep(5)
    return 5
    
start = time()
[y := f(), y**2, y**3]
end = time()
print(f"{(end - start):.2f}s elasped")
5.00s elasped
  1. Update mutatable state in a list comp
total = 0
values = [1,4,10,3,4,5]
partial_sums = [total := total + v for v in values]
print("Total:", total)
Total: 27
  1. In while loops
print("\nwithout the walrus:")
n = 0
while(n < 10):
    n +=1
    print(n, end=",")
    
print("\n\nwith the walrus:")
n = 0
while(n := n +1) < 10:
    print(n, end=",")
without the walrus:
1,2,3,4,5,6,7,8,9,10,

with the walrus:
1,2,3,4,5,6,7,8,9,

What not to do:

def I(x):
    return x

y0 = (y1 := I(5))  # Valid, though discouraged
y0
5
def foo(x):
    return f"I am {x}"

x = "X"
foo(x=(y := I(x))) # confusing
'I am X'

Position-only parameters

# here, a is keyword-only param:
def f(*, a):
    return a + 1

f(1)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-11-83fd13e75879> in <module>
      3     return a + 1
      4 
----> 5 f(1)


TypeError: f() takes 0 positional arguments but 1 was given
f(a=1)
2
# position-only param:
def f(a, /):
    return a + 2

f(a=2)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-15-4a97820f29e2> in <module>
      3     return a + 2
      4 
----> 5 f(a=2)


TypeError: f() got some positional-only arguments passed as keyword arguments: 'a'
f(2)
4

Why would you ever want to do this?

  1. To maintain consistency with existing C functions, like sum, len, divmod, and other builtins
  2. If the names of the parameters are arbitrary, unhelpful, or subject to change
  3. For more flexibility with kwargs
def f(a, b, /, **kwargs):
    print(f"{a=}, {b=}")
    print(f"{kwargs=}")

f(10, 20, a=1, b=2)
a=10, b=20
kwargs={'a': 1, 'b': 2}

Self-documenting f-strings

name = "chuck"
emoj = "🍾"
number = 2

print(f"{name=} {emoj=} {abs(number)=}")
name='chuck' emoj='🍾' abs(number)=2
print(f"{name=!s} {emoj=!a} {abs(number)=:04}")
name=chuck emoj='\U0001f37e' abs(number)=0002

What's new in Python 3.9

Dict merge and update operators

x = {"Washington": "DC", "McLean": "VA"}
y = {"McLean": "?", "New York": "NY"}

print(f"{x | y=}")
print(f"{y | x=}")
x | y={'Washington': 'DC', 'McLean': '?', 'New York': 'NY'}
y | x={'McLean': 'VA', 'New York': 'NY', 'Washington': 'DC'}
y |= {"Gaithersburg": "MD"}
print(f'{y=}')
y={'McLean': '?', 'New York': 'NY', 'Gaithersburg': 'MD'}

Type hinting for generics

Can use list instead of typing.List, e.g.:

def greet_all(names: list[str]) -> None:
    for name in names:
        print("Hello", name)

tuple # typing.Tuple list # typing.List dict # typing.Dict set # typing.Set frozenset # typing.FrozenSet type # typing.Type collections.deque collections.defaultdict collections.OrderedDict collections.Counter collections.ChainMap collections.abc.Awaitable collections.abc.Coroutine collections.abc.AsyncIterable collections.abc.AsyncIterator collections.abc.AsyncGenerator collections.abc.Iterable collections.abc.Iterator collections.abc.Generator collections.abc.Reversible collections.abc.Container collections.abc.Collection collections.abc.Callable collections.abc.Set # typing.AbstractSet collections.abc.MutableSet collections.abc.Mapping collections.abc.MutableMapping collections.abc.Sequence collections.abc.MutableSequence collections.abc.ByteString collections.abc.MappingView collections.abc.KeysView collections.abc.ItemsView collections.abc.ValuesView contextlib.AbstractContextManager # typing.ContextManager contextlib.AbstractAsyncContextManager # typing.AsyncContextManager re.Pattern # typing.Pattern, typing.re.Pattern re.Match # typing.Match, typing.re.Match

graphlib

import graphlib

graph = {"D": {"B", "C"}, "C": {"A"}, "B": {"A"}}
ts = graphlib.TopologicalSorter(graph)
tuple(ts.static_order())
('A', 'C', 'B', 'D')

removeprefix and removesuffix

[f.removeprefix("feature_") for f in features][:5]
['000001', '000002', '000003', '000004', '000005']
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment