What’s New in Python 2020 — Part 1

The ghosts of Python Past (3.7), Present (3.8), and Future (3.9) are visiting you in 2020.

Nicholas Ballard
Towards Data Science

--

Photo by Chris Ried on Unsplash

Since the switch to Python 3 a long time ago (*ahem* — I hope it was a long time ago!), the feature changes at the language level have been relatively minor. Yet with each version, the brainiacs working on Python keep making additions I am unable to live without.

With Python 3.8 released in October 2019, I find myself using features of the language that when I first read about them, made me say “whatever.”

  • 3.5 — type annotations
  • 3.6 — asyncio
  • 3.7 — dataclasses
  • 3.8 — assignment expressions aka walrus operator

And in 3.9, dictionary union operators and generic type hints. Trying to keep the exclamation points to a minimum, but this is exciting stuff!

All the above, I use all the time in code bases professionally, and in my projects for fun.

Quick soapbox speech: If you are still on older versions of Python for work or in your projects, don’t be afraid to upgrade! Your old code will still work, plus you will get the benefits of Python’s new features!

Disclaimer: This is untrue if you’re still using Python 2.7. But in that case, you’re not the type to take good advice anyway. 😎

Below I will go over (quickly) some of my favorite features I hope you will find using in your coding every day.

Those are: type annotations, dataclasses, dictionary union operator, the walrus operator.

In this part: type annotations, the walrus operator.

Typing — 3.5+

Typing has been a feature worked on since the start of Python 3. Since we’re developers, not historians, here will be type annotations and type hints as they stand now (2020).

Python does not need types assigned to variables. That’s probably part of the reason I love the language so much. The clean, readable syntax. The ability to code a solution one of two dozen different ways and still get the same result.

But then… the application grows. Or you have to look at code you haven’t touched in months or years. Or, worst of all, you have to understand code written by someone else! *shudders*

Then you realize typing variables is not for the interpreter’s benefit. It’s for yours.

Typing helps you make sense of your code, as you write it and later. There’s a reason TypeScript has become so popular, even when JavaScript is fully capable of compiling to working code without types.

from typing import List

def print_cats(cats: List[str]) -> None:
for cat in cats:
print(f"{cat} has a name with {len(cat)} letters.")


class Cat(object):
def __init__(self, name: str, age: int, **attrs):
self.cattributes = {
"name": name,
"age": age,
**attrs
}

cats = "this still works w/o type annotation!"
cats: List[str] = ["Meowie", "Fluffy", "Deathspawn"]
# not a list of strings, but Python will not check
cats2: List[str] = [Cat("Meowie", 2), Cat("Deathspawn", 8)]

print_cats(cats) # succeeds
print_cats(cats2) # fails

This returns:

Meowie has a name with 6 letters.
Fluffy has a name with 6 letters.
Deathspawn has a name with 10 letters.
--------------------------------------------
...
TypeError: object of type 'Cat' has no len()

The type annotations did nothing to save us here, so why use them? Because it made it obvious when creating the variable cats and typing it with List[str] that the data assigned should match that structure. So when a function later consumes cats, it becomes (more) obvious the data you are passing it is what it expects.

This becomes more useful — necessary, I would argue — for maintainable code with complex types.

from typing import List


class Cat(object):
def __init__(self, name: str, age: int, **attrs):
self.cattributes = {
"name": name,
"age": age,
**attrs
}

# creating a type variable
Cats: type = List[Cat]


def print_cats(cats: Cats) -> None:
for cat in cats:
name: str = cat.cattributes.get("name")
print(f"{name} has a name with {len(name)} letters.")

cats = [Cat("Meowie", 2), Cat("Deathspawn", 8)]

print_cats(cats)

Output:

Meowie has a name with 6 letters.
Deathspawn has a name with 10 letters.

Typing arguments in the definition of functions / methods is called type hinting. And the type does not even have to be a Python data type or from the typing module. A simple, if awkward, text hint is perfectly legal:

import pandas as pd

cols = ["name", "age", "gender"]
data = [["Meowie", 2, "female"],
["Fluffy", 5, "male"],
["Deathspawn", 8, "rather not say"]]
df: pd.DataFrame = pd.DataFrame() # not very descriptive
df: "name (string), age (integer), gender (string)" = \
pd.DataFrame(data, columns=cols)

Something like that might be useful in a data processing pipeline with lots of variables with complex types, and your head starts spinning trying to keep them straight. IDEs with type hints on variable mouseover would show that hint, too, rather than pandas.DataFrame if it has Python support.

Bonus: In Python 4, forward references will be allowed out of the box. This means you can annotate types not yet defined. We can still use this goodness now by placing from __future__ import annotations at the top of the file, then do something like:

from __future__ import annotations

class Food:
""" Look at the type hint. Food is legal even without the
class defined yet.
"""
def __init__(self, ingred_1: Food, ingred_2: Food) -> None:
self.ingred_1 = ingred_1
self.ingred_2 = ingred_2

Native Type Annotations — 3.9 (soon to be my favorite)

This one will be real quick since I dragged the typing section out.

Builtin generic types will be a thing in 3.9, so importing from typing for adding parameters to generic data types won't be necessary. This has been available with from __futures__ import annotations since 3.7, but that's because it prevents the type reference from being evaluated at runtime.

This gets me excited for upgrading from 3.8. Now I’m importing typing into every module, or importing from a type definition module I keep alongside the code.

Examples (credit: PEP 585):

>>> l = list[str]()
[]
>>> list is list[str]
False
>>> list == list[str]
False
>>> list[str] == list[str]
True
>>> list[str] == list[int]
False
>>> isinstance([1, 2, 3], list[str])
TypeError: isinstance() arg 2 cannot be a parameterized generic
>>> issubclass(list, list[str])
TypeError: issubclass() arg 2 cannot be a parameterized generic
>>> isinstance(list[str], types.GenericAlias)
True
def find(haystack: dict[str, list[int]]) -> int:
...

Walrus Operator — 3.8 (my favorite)

Walrus have eyes : then tusks =.

:= is an assignment expression, new in Python 3.8.

complicated = {
"data": {
"list": [1,2,3],
"other": "stuff"
}
}

if (nums := complicated.get('data').get('list')):
print(nums)

Result:

1
2
3

Without the walrus, this would be more lines of code.

...

nums = complicated.get('data').get('list')
if nums:
print(nums)

Not the end of the world, but since control flow statements are used constantly in programming, once you start using the walrus operator, you won’t stop.

From PEP 572:

The value of such a named expression is the same as the incorporated expression, with the additional side-effect that the target is assigned that value

Killing two statements with one expression, in other words.

While I’m copy/pasting PEP guides, here’s a few more examples from the spec I think are good examples. Can’t wait to try the walrus operator out in a list comprehension.

# Handle a matched regex
if (match := pattern.search(data)) is not None:
# Do something with match

# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
process(chunk)

# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]

# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]

Conclusion

Additions to the Python language recently offer up some pretty decent features to practice. I hope you found my take on typing and the walrus operator useful.

In Part 2 we’ll look at dataclasses, with the builtin library but also look at some reasons to consider pydantic. We will also cover the dictionary union operator, which is a new addition to the syntax coming in Python 3.9.

--

--