The New Features of Python 3.9

  • Proper Time Zone Support

  • Updating Dictionaries with and =
  • Changes to Decorator Syntax

  • Annotated Type Hints

  • PEG Parser

  • String Prefix and Suffix

  • Type Hint Generics

  • Topological Sort v. Greatest Common Divisor and Least Common Multiple

  • New HTTP Status Codes

    (Credits : TheRealPython.com)


TIME ZONES

TIME ZONES PRIOR TO PYTHON 3.9

By default datetime objects have no time zone information

You can add a time zone to a date using the tz attribute in date/time creation methods

>>> from datetime import datetime, timezone
>>> datetime.now()
datetime.datetime(2020, 9, 18, 9, 2, 45, 416684)
>>> datetime.now(tz=timezone.utc)
datetime.datetime(2020, 9, 18, 13, 2, 47, 24553, tzinfo=datetime.timezone.utc)

Timezone provides one timezone: UTC

You can create your own time zone by subclassing and specifying an offset

TIME ZONES IN PYTHON 3.9

Use zoneinfo.ZoneInfo to access your computer’s time zone database

● Can specify time zones by name

● Has hundreds of time zones

● Handles zone name changes during daylight savings time

>>> from datetime import datetime, timezone
>>> datetime.now()
datetime.datetime(2020, 9, 23, 13, 21, 55, 632239)
>>> datetime.now(tz=timezone.utc)
datetime.datetime(2020, 9, 23, 17, 22, 2, 792297, tzinfo=datetime.timezone.utc)
>>> from zoneinfo import ZoneInfo
>>> ZoneInfo("America/Vancouver")
zoneinfo.ZoneInfo(key='America/Vancouver')
>>> datetime.now(tz=ZoneInfo("Europe/Oslo")) '''Current time in Oslo'''
datetime.datetime(2020, 9, 23, 19, 27, 30, 544399, tzinfo=zoneinfo.ZoneInfo(key='Europe/Oslo'))
>>> release_date = datetime(2020, 10, 5, 3, 9, tzinfo=ZoneInfo("America/Vancouver")) '''This is the time in Vancouver'''
>>> release_date.astimezone(ZoneInfo("Europe/Oslo")) '''Now the time in Vancouver is converted to Oslo timestamp using ZoneInfo'''
datetime.datetime(2020, 10, 5, 12, 9, tzinfo=zoneinfo.ZoneInfo(key='Europe/Oslo'))
>>> len(zoneinfo.available_timezones()) '''Available timezones in the database'''
595
>>> tz_kiritimati = ZoneInfo("Pacific/Kiritimati") '''Cool little code, try figuring it out!'''
>>> nye = datetime(1994, 12, 31, 9, 0, tzinfo=ZoneInfo("UTC"))
>>> nye.astimezone(tz_kiritimati)
datetime.datetime(1994, 12, 30, 23, 0, tzinfo=zoneinfo.ZoneInfo(key='Pacific/Kiritimati'))
>>> from datetime import timedelta
>>> hour = timedelta(hours=1)
>>> (nye + hour).astimezone(tz_kiritimati)
datetime.datetime(1995, 1, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='Pacific/Kiritimati'))
>>> tz_kiritimati.utcoffset(datetime(1994, 12, 30))
datetime.timedelta(days=-1, seconds=50400)
>>> tz_kiritimati.utcoffset(datetime(1994, 12, 30)) / hour
-10.0
>>> tz_kiritimati.utcoffset(datetime(1995, 1, 1)) / hour
14.0
>>> tz = ZoneInfo("America/Vancouver")
>>> tz.tzname(datetime(2020, 10, 5))
'PDT'
>>> tz.tzname(datetime(2020, 12, 5))
'PST'

ZoneInfo DATABASE

ZoneInfo gets its time zone information from an IANA database installed on your computer ● Some computers don’t have one installed – particularly Windows

>>> from zoneinfo import ZoneInfo
>>> ZoneInfo("America/Vancouver")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZoneInfoNotFoundError: 'No time zone found with key America/Vancouver'

● A Python maintained copy of the database can be installed

$ python -m pip install tzdata

PYTHON BACKPORTS

ZoneInfo has been included in the backports package and is available for Python 3.6-3.8

$ python -m pip install backports.zoneinfo

Version agnostic library import:

try:
	import zoneinfo
except ImportError:
	from backports import zoneinfo

MERGING AND UPDATING DICTIONARIES

Python 3.9 introduces two new dictionary operators:

● Create a new dictionary based on the merging of two dictionaries without effecting the originals using the operator
● Update a dictionary based on another using the = operator
>>> pycon = {2016: "Portland", 2018: "Cleveland"}
>>> europython = {2017: "Rimini", 2018: "Edinburgh", 2019: "Basel"}
>>> merged = {**pycon, **europython}
>>> pycon
{2016: 'Portland', 2018: 'Cleveland'}
>>> europython
{2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel'}
>>> merged
{2016: 'Portland', 2018: 'Edinburgh', 2017: 'Rimini', 2019: 'Basel'}
>>> merged = pycon.copy()
>>> merged
{2016: 'Portland', 2018: 'Cleveland'}
>>> for key, value in europython.items():
...     merged[key] = value
...
>>> merged
{2016: 'Portland', 2018: 'Edinburgh', 2017: 'Rimini', 2019: 'Basel'}
>>> pycon.update(europython)
>>> pycon
{2016: 'Portland', 2018: 'Edinburgh', 2017: 'Rimini', 2019: 'Basel'}
>>> pycon = {2016:"Portland", 2018:"Cleveland"}
>>> merged = pycon.copy().update(europython)
>>> print(merged)
None'''doesn’t work. And the reason that doesn’t work is because the .update() function operates in place—it does not return a dictionary.So pycon.copy() gets run, the result of that dictionary has .update() called upon it, and then .update() returns nothing. So this short form doesn’t work.You can tackle this problem using the recently introduced walrus operator (:=).'''
>>> (merged := pycon.copy()).update(europython)
>>> merged
{2016: 'Portland', 2018: 'Edinburgh', 2017: 'Rimini', 2019: 'Basel'}
>>> pycon | europython '''Python 3.9 has introduced the pipe operator to make cleaner-looking code to solve the same kind of problem.'''
{2016: 'Portland', 2018: 'Edinburgh', 2017: 'Rimini', 2019: 'Basel'}
>>> pycon
{2016: 'Portland', 2018: 'Cleveland'}
>>> pycon |= europython
>>> pycon
{2016: 'Portland', 2018: 'Edinburgh', 2017: 'Rimini', 2019: 'Basel'}
>>> from collections import defaultdict
>>> europe = defaultdict(lambda: "", {"Norway": "Oslo", "Spain": "Madrid"})
>>> africa = defaultdict(lambda: "", {"Egypt": "Cairo", "Zimbabwe": "Harare"})
>>> {**europe, **africa}
{'Norway': 'Oslo', 'Spain': 'Madrid', 'Egypt': 'Cairo', 'Zimbabwe': 'Harare'}
>>> europe | africa
defaultdict(<function <lambda> at 0x7f9df7b2e5e0>, {'Norway': 'Oslo', 'Spain': 'Madrid', 'Egypt': 'Cairo', 'Zimbabwe': 'Harare'})
>>> libraries = {"collections": "Container datatypes", "math": "Math functions"}
>>> libraries |= [("graphlib", "Topological sort tools")]
>>> libraries
{'collections': 'Container datatypes', 'math': 'Math functions', 'graphlib': 'Topological sort tools'}
>>> asia = {"Georgia": "Tbilisi", "Japan": "Tokyo"}
>>> usa = {"Missouri": "Jefferson City", "Georgia": "Atlanta"}
>>> asia | usa
{'Georgia': 'Atlanta', 'Japan': 'Tokyo', 'Missouri': 'Jefferson City'}
>>> usa | asia
{'Missouri': 'Jefferson City', 'Georgia': 'Tbilisi', 'Japan': 'Tokyo'}

DICTIONARY-LIKE

New operators have been added to:

● UserDict ● ChainMap ● OrderedDict ● defaultdict ● WeakKeyDictionary ● WeakValueDictionary ● _Environ ● MappingProxyType

But: have not been added to the abstract base classes Mapping and MutableMapping.

DUNDER Methods

Operators are implemented by the dunder methods: ● or() ● ior() ● See PEP 584 for details: https://www.python.org/dev/peps/pep-0584/


More Flexible Decorators

A decorator is a callable that wraps a function or class

● Typically used to do something before and/or after a function operates, examples:

● Log information on entry and exit of a function

● Ensure a user is logged in before being allowed access to a web page view

● Prior to Python 3.9, decorators had to be a named callable: only the name of a function or class

● Python 3.9 allows decorators to be any callable expression: anything that resolves to a function or a class

# newdecorators.volume.py
import functools

def normal(func):
    return func

def shout(func):
    @functools.wraps(func)
    def shout_decorator(*args, **kwargs):
        return func(*args, **kwargs).upper()

    return shout_decorator

# newdecorators.before.py
from volume import normal, shout

@normal
def first():
    return "It was the best of times,"

@shout
def second():
    return "it was the worst of times,"


# newdecorators.decorator_exp.py
from volume import normal, shout

DECORATORS = {
    "normal":normal,
    "shout":shout,
}

voice = input("Choose: ")

@DECORATORS[voice]
def third():
    return "In a hole in the ground there lived a hobbit."


ANNOTATIONS

Python 3 has syntax for annotations

● Originally used to provide extra information about a parameter, for example the units of a variable

def speed(distance: "feet", time: "seconds") -> "miles per hour":
"""Calculate speed as distance over time"""
fps2mph = 3600 / 5280 # Feet per second to miles per hour
return distance / time * fps2mph

PEP 484 introduced the use of annotations for type hints

● Type hints have become the most common use of annotations

● Python 3.9 introduces the typing.Annotated class allowing you to annotate with both a type hint and meta information


PEG PARSER

Python originally used an LL(1) parser

● Reads a single character at a time and doesn’t backtrack

● One of the simpler types of parsers

● Straight forward to implement

● There are some corner cases

● Python 3.9 has added a Parsing Expression Grammar (PEG) parser

● Avoids some of the corner cases

● Both parsers are available and for Python 3.9 should produce the same Abstract Syntax Tree (AST)

OLD PARSER

The LL(1) parser can be used through the -X oldparser command line flag

$ python -X oldparser script_name.py

Old parser will be removed in Python 3.10

● Opens up the grammar to introduce new features, like PEP 622 Structural Pattern Matching


GENERIC TYPE HINTS

Type hints for basic types could be annotated directly using keywords like: str, int, and bool

● Type hints for generics like lists used to require a special type typing.List

● Python 3.9 removes this need, you can now use list

OLD vs NEW HINT LISTS

Before Python 3.9

from typing import List
radius: float = 3.9
radii: List[float] = [3.9, 2.4]

Python 3.9:

radius: float = 3.9
radii: list[float] = [3.9, 2.4]		

REACH INTO THE __future__

Delayed evaluation of type annotations is available in future

● Using future you take advantage of the new syntax in Python 3.7 & 3.8

from __future__ import annotations
radius: float = 3.9
radii: list[float] = [3.9, 2.4]