Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🐍 Python: Tips & Tricks

How to Write More Pythonic Code

📽 Recording


Table of Contents


🪷 Zen of Python

importthis
>>> The Zen of Python, by Tim Peters
>>> Beautiful is better than ugly.
>>> Explicit is better than implicit.
>>> Simple is better than complex.
>>> Complex is better than complicated.
>>> Flat is better than nested.
>>> Sparse is better than dense.
>>> Readability counts.
>>> Special cases aren't special enough to break the rules.
>>> Although practicality beats purity.
>>> Errors should never pass silently.
>>> Unless explicitly silenced.
>>> In the face of ambiguity, refuse the temptation to guess.
>>> There should be one-- and preferably only one --obvious way to do it.
>>> Although that way may not be obvious at first unless you're Dutch.
>>> Now is better than never.
>>> Although never is often better than *right* now.
>>> If the implementation is hard to explain, it's a bad idea.
>>> If the implementation is easy to explain, it may be a good idea.
>>> Namespaces are one honking great idea -- let's do more of those!

📝 Python History

  • 3.6 (Dec 23, 2016): f-strings
  • 3.7 (Jun 27, 2018): data classes
  • 3.8 (Oct 14, 2019): walrus operator
  • 3.9 (Oct 5, 2020): simpler dictionary updates/merges
  • 3.10 (Oct 4, 2021): pattern matching
  • 3.11 (Oct 3, 2022): performance increases (~25%)

🎻 String Formatting

There are 4 ways to do that in Python, with a single preferred one.


The Simple (+)

🚫 Plain approach to glue strings together:

name="Bob"greeting="hello"message=greeting+" there, "+name+"!"message>>>'hello there, Bob!'

ℹ️ One can only concatenate strings with strings:

units=10items="apples"print("Currently in stock: "+str(units) +" "+items)
>>>'Currently in stock: 10 apples'

The Old (%)

🚫

name="Bob"greeting="hello"message="%s there, %s!"% (greeting, name)
message>>>'hello there, Bob!'# same result:message="%(greeting)s, %(name)s!"% {"greeting": greeting, "name": name}

The Meh (.format)

🚫

name="Bob"greeting="hello"message="{} there, {}!".format(greeting, name)
message>>>'hello there, Bob!'# can enumerate params:message="{0} there, {1}!".format(greeting, name)
message="{1} there, {0}!".format(name, greeting)
>>>'hello there, Bob!'# same result:message="{greeting} there, {name}!".format(greeting=greeting, name=name)

The Good (f-strings)

Added in Python 3.6:

✅ Looks better, more powerful, better performance (2x faster than format, 50% faster than %):

name="Bob"greeting="hello"message=f"{greeting} there, {name}!"message>>>'hello there, Bob!'

ℹ️ Any Python expressions and value formatting support:

importmathr=2print(f"Circle of radius {r} has a circumference of {2*math.pi*r}")
>>>Circleofradius2hasacircumferenceof12.566370614359172print(f"Circle of radius {r} has a circumference of {2*math.pi*r:.2f}")
>>>Circleofradius2hasacircumferenceof12.57

ℹ️ Debugging specifier = (added in Python 3.8):

x=123; y=456print(f"Calculated values: x={x}, y={y}")
>>>Calculatedvalues: x=123, y=456print(f"Calculated values: {x=}, {y=}")
>>>Calculatedvalues: x=123, y=456data= {'city': 'Berlin', 'country': 'DE'}
print(f"Result: {data=}")
>>>Result: data={'city': 'Berlin', 'country': 'DE'}

🌱 Basic Data Structures


  • Lists
  • Strings
  • Dicts
  • Tuples
  • Sets

Lists


A list is a mutable, ordered array of values

data= [1, 3, 5]
data.append(7)
data>>> [1, 3, 5, 7]
data.extend([9, 11]) # same as: data += [9, 11]>>> [1, 3, 5, 7, 9, 11]
len(data)
>>>6

Iteration

🚫 Index-based iteration loops:

foriinrange(len(data)):
print(data[i])
>>>1>>>3>>>5

✅ Every list is iterable:

forxindata:
print(x)
>>>1>>>3>>>5

ℹ️ In case one needs to access the current element's index:

fori, xinenumerate(data):
print(f"Element {i}: {x}")
>>>Element0: 1>>>Element1: 3>>>Element2: 5

Math Operations

🚫

data= [1, 2, -3, 4, 5]
sum_=0min_=data[0]
max_=data[0]
forxindata:
sum_+=xifx<min_:
min_=xifx>max_:
max_=xsum_>>>9min_>>>-3max_>>>5

data= [1, 2, -3, 4, 5]
sum(data)
>>>9min(data)
>>>-3max(data)
>>>5

List Slicing [::]

Done with so-called 🍣 sushi-operator ([::]):

array[<start_index>:<stop_index>:<step>]
  • start_index = 0 if not specified
  • stop_index = len(array) if not specified
    • it's exclusive: stop_index value is not included in the slice result
  • step_index = 1 if not specified

data= [2, 4, 6, 8, 10]
# index: 0 1 2 3 4data[1:] # same as [1::] or [1:5:1]>>> [4, 6, 8, 10]
data[1:3] # same as [1:3:1]>>> [4, 6]
data[::2] # same as [0:5:2]>>> [2, 6, 10]
data==data[0:5:1]
>>>Truedata==data[:]
>>>True

Slicing the full list with step -1 (backwards) returns a reversed version of the list:

data= [2, 4, 6, 8, 10]
data[::-1]
>>> [10, 8, 6, 4, 2]

Membership Testing With in

🚫 Implement searching algorithm yourself:

array= [1, 2, 3, 4, 5]
search_for=3found=Falseforiinrange(len(array)):
ifarray[i] ==search_for:
found=Truebreakprint(f"Found: {found}")
>>>True

✅ Let Python do it:

array= [1, 2, 3, 4, 5]
search_for=3found=search_forinarrayprint(f"Found: {found}")
>>>True

List Comprehension

Formula: [value for item in iterable] (for every item in iterable map it to value)

# range(A, B, C) = iterator of integer sequence from A to B with a step C (B is excluded) data= [x**2forxinrange(0, 5)]
data>>> [0, 1, 4, 9, 16]

List comprehension with a condition (formula: [value for item in iterable if condition])

data= [3, 2, -5, 10, 21, 7]
even= [xforxindataifx%2==0]
even>>> [2, 10]

Mapping and Filtering

Alternative to list comprehension is to use map (with a lambda function (inline function))

data=map(lambdax: x**2, range(0, 5))
print(list(data)) # `map` returns an iterator, `list` creates a materialized list of it>>> [0, 1, 4, 9, 16]

Alternative to list comprehension with a condition is to use filter (with a lambda function (inline function)):

data= [3, 2, -5, 10, 21, 7]
even=filter(lambdax: x%2==0, data)
list(even) # `filter` returns an iterator, `list` creates a materialized list of it>>> [2, 10]

Sorting Lists

data= [
{'city': 'Paris', 'country': 'FR'},
{'city': 'Berlin', 'country': 'DE'},
{'city': 'London', 'country': 'UK'}
]
# order by city name:sorted(data, key=lambdax: x['city'])
>>> [{'city': 'Berlin', 'country': 'DE'}, {'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}]
# order by country code reversed:sorted(data, key=lambdax: x['country'], reverse=True)
>>> [{'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}, {'city': 'Berlin', 'country': 'DE'}]

Truthiness

🚫 Check if the list is empty/not empty:

iflen(data) ==0:
print("List is empty")
iflen(data) >0:
print("List is not empty")

ifnotdata:
print("List is empty")
ifdata:
print("List is not empty")

Flatten List of lists

regular_list= [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
flat_list= [itemforsublistinregular_listforiteminsublist]
print('Original list', regular_list)
>>>Originallist [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
print('Transformed list', flat_list)
>>>Transformedlist [1, 2, 3, 4, 5, 6, 7, 8, 9]

Strings


A string can be seen as an iterable list of characters:

data='oslo'forletterindata:
print(letter.upper())
>>>O>>>S>>>L>>>Odata[2]
>>>'l'len(data)
>>>4data[::-1]
>>>'olso'
[ord(x) forxindata] # ord(x) == Unicode integer of character x>>> [111, 115, 108, 111]

Membership Check With in

🚫 Is substring in string:

"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False

"aura"in"restaurant">>>True"fun"in"waterfall">>>False

Stripping Whitespace Characters

data=' empty spaces, what are we living for? 'print(data.strip())
>>>'empty spaces, what are we living for?'print(data.rstrip())
>>>' empty spaces, what are we living for?'print(data.lstrip())
>>>'empty spaces, what are we living for? '

Prefix/Suffix Manipulations

Added in Python 3.9:

print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'

Tokenization

string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']

Dicts


Dict is key-val storage:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'

Iteration

Any dict is iterable:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
# iterate other keysforkeyindata:
print(f"{key}: {data[key]}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']
# iterate over keys with values:forkey, valindata.items():
print(f"{key}: {val}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']

Membership Check With in

🚫

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
found=Falseforkeyindata:
ifkey=='city':
found=Truebreakfound>>>True

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
'city'indata>>>True'population'indata>>>False'continent'notindata>>>True

Dict Comprehension

Formula: {key: val for item in iterable}

data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}

Accessing Values: [] vs. get

data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexception

data= {'a': 123, 'b': 456}
data.get('a')
>>>123data.get('b')
>>>456data.get('c')
>>>Nonedata.get('c', 'default value')
>>>'default value'

FYI, there's no set for dicts, values to be changes with the [] notation only (e.g. data["key"] = "val")


Safe Navigation

🚫 Error-prone code:

data= {'a': {'b': {'c': 123}}}
element=data['a']['b']['c']
element>>>123data= {'a': {'d': 456}}
element=data['a']['b']['c']
>>>KeyError!

✅ Robust way to inspect a dictionary:

data= {'a': {'d': 456}}
element=data.get('a', {}).get('b', {}).get('c')
element>>>None

Merging

europe= {'Madrid': 'Spain', 'Rome': 'Italy'}
asia= {'Tokyo': 'Japan', 'Manila': 'Philippines'}

Before Python 3.9:

{**europe, **asia}
>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Starting Python 3.9:

europe|asia>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Tuples


Tuple is an immutable, ordered array of values:

data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignment

Tuples can be implicit:

a=1, 2a[0]
>>>1# the same is:a= (1, 2)

Sets


Set is a unordered array of unique values:

data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptable

🧑‍🔧 Data Structures: Examples


🦄 Get Unique Elements of a List

Converting a list to a set removes duplicates:

data= [5, 2, 3, 2, 4, 3, 1]
unique=list(set(data))
unique>>> [1, 2, 3, 4, 5]

🔍 Search in a List of Objects

data= [
{"city": "Berlin", "country": "DE"},
{"city": "Sydney", "country": "AU"},
{"city": "Stockholm", "country": "SE"}
]
search=next((itemforitemindataifitem["city"] =="Sydney"), None)
search["country"]
>>>'AU'search=next((itemforitemindataifitem["city"] =="Paris"), None)
searchisNone>>>True

🪡 Is Any of the Values

🚫

ifoperation=="READ"oroperation=="WRITE":

ifoperationin ["READ", "WRITE"]:

📦 Values Unpacking


🔁 Variable Swapping

🚫 Using a temporary variable:

a=5; b=4tmp=aa=bb=tmpa>>>4b>>>5

✅ Cut to the chase:

a=5; b=4a, b=b, a# same as a, b = (b, a)a>>>4b>>>5

📭 Unpacking With Lists and Tuples

🚫

data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two

data= ['one', 'two']
a, b=dataa>>>oneb>>>two

Also works for tuples:

data= ('one', 'two', 'many', 'things')
a, b, *c=datac>>> ['many', 'things']
a, b=123, 456# same as a, b = (123, 456)a>>>123b>>>456

🌾 Data Classes


🚫 Describing objects can be done with dicts:

d1= {'name': 'Moabit', 'city': 'Berlin', 'country': 'DE', 'area': 7.72}
d2= {'name': 'Greenwich', 'city': 'London', 'country': 'UK', 'area': 47.3}
d2['city']
>>>'London'

Problem: no type hinting possible (e.g. that name is str and area should be a float) and there are no object structure restrictions in place.


🚫 We can use classes for solving it but that's bit too verbose:

classDistrict:
def__init__(self, name: str, city: str, country: str, area: float):
self.name: str=nameself.city: str=cityself.country: str=countryself.area: str=aread1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d2.city>>>'London'

✅ Using data classes (added in Python 3.7):

fromdataclassesimportdataclass@dataclassclassDistrict:
name: strcity: strcountry: strarea: float=0.0d1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d3=District(name='Brooklyn', city='New York', country='US')
d2.city>>>'London'd3.area>>>0.0

🦆 Type Hinting

There's no run-time type checking, but code with type hints allows:

  • IDEs (e.g. PyCharm) and static type checkers (e.g. mypy) to catch errors before runtime
  • to have a better, self-documented code

Basic Type Hinting

🚫

defsum_values(a, b):
returna+bsum_values(10, 3)
>>>13sum_values(10, "x")
>>>TypeError: unsupportedoperand type(s) for+: 'int'and'str'

defsum_values(a: int, b: int) ->int:
returna+bsum_values(10, "x") # IDE will highlight an error

Type Hinting & Multiple Types

Before Python 3.10:

fromtypingimportUniondefsum_values(a: Union[int, float], b: Union[int, float]) ->Union[int, float]:
# a and b can be either int or floatreturna+b

Starting Python 3.10:

defsum_values(a: int|float, b: int|float) ->int|float:
returna+b

Type Hinting & Containers

Before Python 3.10:

fromtypingimportDict, Listdefmake_list(a: str, b: str) ->List[str]:
# return type is a list of stringsreturn [a, b]
defmake_dict(k: str, v: str) ->Dict[str, str]:
# return type is a dict with string keys and string valuesreturn {k: v}
make_list("hello", "world")
>>> ['hello', 'world']
make_dict("hello", "world")
>>> {'hello': 'world'}

Starting Python 3.10:

defmake_list(a: str, b: str) ->list[str]:
return [a, b]
defmake_dict(k: str, v: str) ->dict[str, str]:
return {k: v}

Type Hinting & Generic Objects

Before Python 3.10:

fromdataclassesimportdataclassfromtypingimportList@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: List[City]) ->List[City]:
returnsorted(cities, key=lambdax: x.name)
cities= [
City(name='Madrid', country='ES'),
City(name='Berlin', country='DE'),
City(name='Edinburgh', country='UK')
]
sort_cities(cities)
>>> [City(name='Berlin', country='DE'), City(name='Edinburgh', country='UK'), City(name='Madrid', country='ES')]

Starting Python 3.10:

fromdataclassesimportdataclass@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: list[City]) ->list[City]:
returnsorted(cities, key=lambdax: x.name)

Type Hinting and Local Variables

Not only limited to inputs/outputs of a function:

Before Python 3.10:

fromtypingimportListresult: List[str] = [] # not just a list of anything!

Starting Python 3.10:

result: list[str] = []

👷‍♂️ Operators


👯‍♀️ Double Comparison

🚫

ifvalue>0andvalue<100:

if0<value<100:

🧬 Pattern Matching

Added in Python 3.10. Similar to switch statements in other languages, on steroids:

defparse_command(command: str) ->str:
matchcommand.split():
case [action, direction]:
returnf"Parsed: {action=}, {direction=}"case ["help"]: return"Help message goes here"case _:
return"Wrong command, 2 words expected"parse_command("go north")
>>>"Parsed: action='go', direction='north'"parse_command("look up")
>>>"Parsed: action='look', direction='up'"parse_command("go")
>>>"Wrong command, 2 words expected"parse_command("help")
>>>"Help message goes here"

Alias matching with as, OR matching with | and conditional matching:

defparse_command(command: str) ->str:
matchcommand.split():
case ["go", ("north"|"south") asdirection]:
returnf"Going {direction}"case ["go", _]: return"Sorry, can't go there!"case (["pick", obj, "up"] | ["pick", "up", obj]) ifobjin ['shovel', 'rock']:
returnf"Picking up {obj}"case ["pick", _, "up"] | ["pick", "up", _]:
return"Sorry, can't pick this up!"case _:
return"Wrong command, 2 words expected"parse_command("go south")
>>>"Going south"parse_command("go left")
>>>"Sorry, can't go there!"parse_command("pick shovel up")
>>>"Picking up shovel"parse_command("pick phone up")
>>>"Sorry, can't pick this up!"

Adapting to different structure types:

fromdataclassesimportdataclassfromdatetimeimportdatetime@dataclassclassUser:
age: intdefget_age(user: dict|User) ->int:
matchuser:
caseUser(age):
returnagecase {"dob": {"age": int(age) |float(age)}}:
returnint(age)
case {"dob": dob}:
now=datetime.now()
dob_date=datetime.strptime(dob, "%Y-%m-%d %H:%M:%S")
returnnow.year-dob_date.year
get_age({"dob": "1966-04-17 11:57:01"})
>>>56get_age({"dob": {"date": "1957-05-20T08:36:09.083Z", "age": 64}})
>>>64get_age({"dob": {"age": 39.6}})
>>>39get_age(User(age=40))
>>>40

🍴 Ternary Operator

ifa==5:
result="Five!"else:
result="Not five..."

Shorter way to write the same:

result="Five!"ifa==5else"Not five..."

🦷 Walrus Operator :=

Added in Python 3.8:

value=123print(value)
>>>123# can be written as:print(value:=123)
>>>123value>>>123

😒 Can be fine, but not the most concise way:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
# get some stats on the list: length, sum, mean valuesnum_length=len(numbers)
num_sum=sum(numbers)
stats= {
"length": num_length,
"sum": num_sum,
"mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

✅ Doing the same with less lines of code:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
stats= {
"length": (num_length:=len(numbers)), "sum": (num_sum:=sum(numbers)), "mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

🛂 == vs is

  • ==: do two objects have the same contents?
  • is: are two objects the same thing (point to the same address in memory)?
a= [1, 2, 3]
b= [1, 2, 3]
a==b>>>True
id(a) # Python id of object a>>>4435362944id(b) # Python id of object b>>>4435377344aisb# same as id(a) == id(b)>>>Falsea=baisb>>>True

ℹ️ As a consequence:

a= [1, 2, 3]
b= [1, 2, 3]
a[0] =4print(a, b)
>>> [4, 2, 3] [1, 2, 3]
a=b# make a point to the same object as b, not copying contents of b!a[0] =4# also changes b now as a and b point to the same address in memoryprint(a, b)
>>> [4, 2, 3] [4, 2, 3]

Copying an Object

🚫 Looks cryptic, and only works for lists but not for e.g. dicts:

a= [1, 2, 3]
b=a[:]
a==b>>>Trueaisb>>>False

a= [1, 2, 3]
b=a.copy()
a==b>>>Trueaisb>>>False

ℹ️ There's only one global None object

c=Noned=Nonec==d>>>Truecisd# c and d and not "copies" of None, they point to it>>>True

🧮 Named Parameters

defprint_issue_info(issue_id: str, issue_title: str)
print(f"Issue id: {issue_id}, title: {issue_title}")

😒 Can be fine:

print_issue_info("1234", "Create new thing")
>>>"Issue id: 1234, title: Create new thing"

✅ More explicit and human-readable:

print_issue_info(issue_id="1234", issue_title="Create new thing")
>>>"Issue id: 1234, title: Create new thing"print_issue_info(issue_title="Create new thing", issue_id="1234")
>>>"Issue id: 1234, title: Create new thing"

🏀 Practical Examples


📁 Reading/Writing Files

🚫 Handle errors yourself:

f=open('data.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()

✅ Use a context managerwith:

withopen("data.txt", "r") asf:
data=f.read()
withopen("data2.txt", "w") asf:
f.write(data)

🅾️ Loading/Exporting JSON

json is a built-in Python module:

importjsondata= {"a": 123, "b": None}
data_json=json.dumps(data) # dumps = dump stringprint(data_json)
>>> {"a": 123, "b": null}
data_parsed=json.loads(data_json) # loads = load stringprint(data_parsed==data)
>>>True

Reading JSON Files

$ cat file.json
{
"a": {"b": 123}
}
importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123

🌍 HTTP Requests

✅ Use the requests library:

importrequestsurl='https://api.github.com/some/endpoint'headers= {'Authentication': 'Bearer mytoken'}
r=requests.get(url, headers=headers)
print(r.status_code)
>>>200print(r.json())
>>> {"status": "OK", "message": "hi from the API"}

🔨 Tools


🔁 REPL

REPL = read-eval-print loop

Can be used to quickly try things out in a terminal:

$ python3
Python 3.10.6 (main, Aug 11 2022, 13:49:25) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license"for more information.
>>> sum(range(0, 10))
45

🐞 Debugging

breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):

value=123breakpoint()
(Pdb) value>>>123

🏁 Outro: Key Advice

  • Write as short and lean code as possible
  • Use the most recent version of Python
  • Try ideas with REPL quickly
  • Use type hinting
  • Know your basic data structures
  • Use list & dict comprehensions
  • Use f-strings
  • Use data classes
  • RealPython.com is a great source of guide on specific topics (example)

🙇‍♂️ Thank You!

About

Slides for tech talk in Camunda on Python tips and tricks (Sep 2022)

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🐍 Python: Tips & Tricks

How to Write More Pythonic Code

📽 Recording


Table of Contents


🪷 Zen of Python

importthis
>>> The Zen of Python, by Tim Peters
>>> Beautiful is better than ugly.
>>> Explicit is better than implicit.
>>> Simple is better than complex.
>>> Complex is better than complicated.
>>> Flat is better than nested.
>>> Sparse is better than dense.
>>> Readability counts.
>>> Special cases aren't special enough to break the rules.
>>> Although practicality beats purity.
>>> Errors should never pass silently.
>>> Unless explicitly silenced.
>>> In the face of ambiguity, refuse the temptation to guess.
>>> There should be one-- and preferably only one --obvious way to do it.
>>> Although that way may not be obvious at first unless you're Dutch.
>>> Now is better than never.
>>> Although never is often better than *right* now.
>>> If the implementation is hard to explain, it's a bad idea.
>>> If the implementation is easy to explain, it may be a good idea.
>>> Namespaces are one honking great idea -- let's do more of those!

📝 Python History

  • 3.6 (Dec 23, 2016): f-strings
  • 3.7 (Jun 27, 2018): data classes
  • 3.8 (Oct 14, 2019): walrus operator
  • 3.9 (Oct 5, 2020): simpler dictionary updates/merges
  • 3.10 (Oct 4, 2021): pattern matching
  • 3.11 (Oct 3, 2022): performance increases (~25%)

🎻 String Formatting

There are 4 ways to do that in Python, with a single preferred one.


The Simple (+)

🚫 Plain approach to glue strings together:

name="Bob"greeting="hello"message=greeting+" there, "+name+"!"message>>>'hello there, Bob!'

ℹ️ One can only concatenate strings with strings:

units=10items="apples"print("Currently in stock: "+str(units) +" "+items)
>>>'Currently in stock: 10 apples'

The Old (%)

🚫

name="Bob"greeting="hello"message="%s there, %s!"% (greeting, name)
message>>>'hello there, Bob!'# same result:message="%(greeting)s, %(name)s!"% {"greeting": greeting, "name": name}

The Meh (.format)

🚫

name="Bob"greeting="hello"message="{} there, {}!".format(greeting, name)
message>>>'hello there, Bob!'# can enumerate params:message="{0} there, {1}!".format(greeting, name)
message="{1} there, {0}!".format(name, greeting)
>>>'hello there, Bob!'# same result:message="{greeting} there, {name}!".format(greeting=greeting, name=name)

The Good (f-strings)

Added in Python 3.6:

✅ Looks better, more powerful, better performance (2x faster than format, 50% faster than %):

name="Bob"greeting="hello"message=f"{greeting} there, {name}!"message>>>'hello there, Bob!'

ℹ️ Any Python expressions and value formatting support:

importmathr=2print(f"Circle of radius {r} has a circumference of {2*math.pi*r}")
>>>Circleofradius2hasacircumferenceof12.566370614359172print(f"Circle of radius {r} has a circumference of {2*math.pi*r:.2f}")
>>>Circleofradius2hasacircumferenceof12.57

ℹ️ Debugging specifier = (added in Python 3.8):

x=123; y=456print(f"Calculated values: x={x}, y={y}")
>>>Calculatedvalues: x=123, y=456print(f"Calculated values: {x=}, {y=}")
>>>Calculatedvalues: x=123, y=456data= {'city': 'Berlin', 'country': 'DE'}
print(f"Result: {data=}")
>>>Result: data={'city': 'Berlin', 'country': 'DE'}

🌱 Basic Data Structures


  • Lists
  • Strings
  • Dicts
  • Tuples
  • Sets

Lists


A list is a mutable, ordered array of values

data= [1, 3, 5]
data.append(7)
data>>> [1, 3, 5, 7]
data.extend([9, 11]) # same as: data += [9, 11]>>> [1, 3, 5, 7, 9, 11]
len(data)
>>>6

Iteration

🚫 Index-based iteration loops:

foriinrange(len(data)):
print(data[i])
>>>1>>>3>>>5

✅ Every list is iterable:

forxindata:
print(x)
>>>1>>>3>>>5

ℹ️ In case one needs to access the current element's index:

fori, xinenumerate(data):
print(f"Element {i}: {x}")
>>>Element0: 1>>>Element1: 3>>>Element2: 5

Math Operations

🚫

data= [1, 2, -3, 4, 5]
sum_=0min_=data[0]
max_=data[0]
forxindata:
sum_+=xifx<min_:
min_=xifx>max_:
max_=xsum_>>>9min_>>>-3max_>>>5

data= [1, 2, -3, 4, 5]
sum(data)
>>>9min(data)
>>>-3max(data)
>>>5

List Slicing [::]

Done with so-called 🍣 sushi-operator ([::]):

array[<start_index>:<stop_index>:<step>]
  • start_index = 0 if not specified
  • stop_index = len(array) if not specified
    • it's exclusive: stop_index value is not included in the slice result
  • step_index = 1 if not specified

data= [2, 4, 6, 8, 10]
# index: 0 1 2 3 4data[1:] # same as [1::] or [1:5:1]>>> [4, 6, 8, 10]
data[1:3] # same as [1:3:1]>>> [4, 6]
data[::2] # same as [0:5:2]>>> [2, 6, 10]
data==data[0:5:1]
>>>Truedata==data[:]
>>>True

Slicing the full list with step -1 (backwards) returns a reversed version of the list:

data= [2, 4, 6, 8, 10]
data[::-1]
>>> [10, 8, 6, 4, 2]

Membership Testing With in

🚫 Implement searching algorithm yourself:

array= [1, 2, 3, 4, 5]
search_for=3found=Falseforiinrange(len(array)):
ifarray[i] ==search_for:
found=Truebreakprint(f"Found: {found}")
>>>True

✅ Let Python do it:

array= [1, 2, 3, 4, 5]
search_for=3found=search_forinarrayprint(f"Found: {found}")
>>>True

List Comprehension

Formula: [value for item in iterable] (for every item in iterable map it to value)

# range(A, B, C) = iterator of integer sequence from A to B with a step C (B is excluded) data= [x**2forxinrange(0, 5)]
data>>> [0, 1, 4, 9, 16]

List comprehension with a condition (formula: [value for item in iterable if condition])

data= [3, 2, -5, 10, 21, 7]
even= [xforxindataifx%2==0]
even>>> [2, 10]

Mapping and Filtering

Alternative to list comprehension is to use map (with a lambda function (inline function))

data=map(lambdax: x**2, range(0, 5))
print(list(data)) # `map` returns an iterator, `list` creates a materialized list of it>>> [0, 1, 4, 9, 16]

Alternative to list comprehension with a condition is to use filter (with a lambda function (inline function)):

data= [3, 2, -5, 10, 21, 7]
even=filter(lambdax: x%2==0, data)
list(even) # `filter` returns an iterator, `list` creates a materialized list of it>>> [2, 10]

Sorting Lists

data= [
{'city': 'Paris', 'country': 'FR'},
{'city': 'Berlin', 'country': 'DE'},
{'city': 'London', 'country': 'UK'}
]
# order by city name:sorted(data, key=lambdax: x['city'])
>>> [{'city': 'Berlin', 'country': 'DE'}, {'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}]
# order by country code reversed:sorted(data, key=lambdax: x['country'], reverse=True)
>>> [{'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}, {'city': 'Berlin', 'country': 'DE'}]

Truthiness

🚫 Check if the list is empty/not empty:

iflen(data) ==0:
print("List is empty")
iflen(data) >0:
print("List is not empty")

ifnotdata:
print("List is empty")
ifdata:
print("List is not empty")

Flatten List of lists

regular_list= [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
flat_list= [itemforsublistinregular_listforiteminsublist]
print('Original list', regular_list)
>>>Originallist [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
print('Transformed list', flat_list)
>>>Transformedlist [1, 2, 3, 4, 5, 6, 7, 8, 9]

Strings


A string can be seen as an iterable list of characters:

data='oslo'forletterindata:
print(letter.upper())
>>>O>>>S>>>L>>>Odata[2]
>>>'l'len(data)
>>>4data[::-1]
>>>'olso'
[ord(x) forxindata] # ord(x) == Unicode integer of character x>>> [111, 115, 108, 111]

Membership Check With in

🚫 Is substring in string:

"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False

"aura"in"restaurant">>>True"fun"in"waterfall">>>False

Stripping Whitespace Characters

data=' empty spaces, what are we living for? 'print(data.strip())
>>>'empty spaces, what are we living for?'print(data.rstrip())
>>>' empty spaces, what are we living for?'print(data.lstrip())
>>>'empty spaces, what are we living for? '

Prefix/Suffix Manipulations

Added in Python 3.9:

print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'

Tokenization

string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']

Dicts


Dict is key-val storage:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'

Iteration

Any dict is iterable:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
# iterate other keysforkeyindata:
print(f"{key}: {data[key]}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']
# iterate over keys with values:forkey, valindata.items():
print(f"{key}: {val}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']

Membership Check With in

🚫

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
found=Falseforkeyindata:
ifkey=='city':
found=Truebreakfound>>>True

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
'city'indata>>>True'population'indata>>>False'continent'notindata>>>True

Dict Comprehension

Formula: {key: val for item in iterable}

data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}

Accessing Values: [] vs. get

data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexception

data= {'a': 123, 'b': 456}
data.get('a')
>>>123data.get('b')
>>>456data.get('c')
>>>Nonedata.get('c', 'default value')
>>>'default value'

FYI, there's no set for dicts, values to be changes with the [] notation only (e.g. data["key"] = "val")


Safe Navigation

🚫 Error-prone code:

data= {'a': {'b': {'c': 123}}}
element=data['a']['b']['c']
element>>>123data= {'a': {'d': 456}}
element=data['a']['b']['c']
>>>KeyError!

✅ Robust way to inspect a dictionary:

data= {'a': {'d': 456}}
element=data.get('a', {}).get('b', {}).get('c')
element>>>None

Merging

europe= {'Madrid': 'Spain', 'Rome': 'Italy'}
asia= {'Tokyo': 'Japan', 'Manila': 'Philippines'}

Before Python 3.9:

{**europe, **asia}
>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Starting Python 3.9:

europe|asia>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Tuples


Tuple is an immutable, ordered array of values:

data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignment

Tuples can be implicit:

a=1, 2a[0]
>>>1# the same is:a= (1, 2)

Sets


Set is a unordered array of unique values:

data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptable

🧑‍🔧 Data Structures: Examples


🦄 Get Unique Elements of a List

Converting a list to a set removes duplicates:

data= [5, 2, 3, 2, 4, 3, 1]
unique=list(set(data))
unique>>> [1, 2, 3, 4, 5]

🔍 Search in a List of Objects

data= [
{"city": "Berlin", "country": "DE"},
{"city": "Sydney", "country": "AU"},
{"city": "Stockholm", "country": "SE"}
]
search=next((itemforitemindataifitem["city"] =="Sydney"), None)
search["country"]
>>>'AU'search=next((itemforitemindataifitem["city"] =="Paris"), None)
searchisNone>>>True

🪡 Is Any of the Values

🚫

ifoperation=="READ"oroperation=="WRITE":

ifoperationin ["READ", "WRITE"]:

📦 Values Unpacking


🔁 Variable Swapping

🚫 Using a temporary variable:

a=5; b=4tmp=aa=bb=tmpa>>>4b>>>5

✅ Cut to the chase:

a=5; b=4a, b=b, a# same as a, b = (b, a)a>>>4b>>>5

📭 Unpacking With Lists and Tuples

🚫

data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two

data= ['one', 'two']
a, b=dataa>>>oneb>>>two

Also works for tuples:

data= ('one', 'two', 'many', 'things')
a, b, *c=datac>>> ['many', 'things']
a, b=123, 456# same as a, b = (123, 456)a>>>123b>>>456

🌾 Data Classes


🚫 Describing objects can be done with dicts:

d1= {'name': 'Moabit', 'city': 'Berlin', 'country': 'DE', 'area': 7.72}
d2= {'name': 'Greenwich', 'city': 'London', 'country': 'UK', 'area': 47.3}
d2['city']
>>>'London'

Problem: no type hinting possible (e.g. that name is str and area should be a float) and there are no object structure restrictions in place.


🚫 We can use classes for solving it but that's bit too verbose:

classDistrict:
def__init__(self, name: str, city: str, country: str, area: float):
self.name: str=nameself.city: str=cityself.country: str=countryself.area: str=aread1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d2.city>>>'London'

✅ Using data classes (added in Python 3.7):

fromdataclassesimportdataclass@dataclassclassDistrict:
name: strcity: strcountry: strarea: float=0.0d1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d3=District(name='Brooklyn', city='New York', country='US')
d2.city>>>'London'd3.area>>>0.0

🦆 Type Hinting

There's no run-time type checking, but code with type hints allows:

  • IDEs (e.g. PyCharm) and static type checkers (e.g. mypy) to catch errors before runtime
  • to have a better, self-documented code

Basic Type Hinting

🚫

defsum_values(a, b):
returna+bsum_values(10, 3)
>>>13sum_values(10, "x")
>>>TypeError: unsupportedoperand type(s) for+: 'int'and'str'

defsum_values(a: int, b: int) ->int:
returna+bsum_values(10, "x") # IDE will highlight an error

Type Hinting & Multiple Types

Before Python 3.10:

fromtypingimportUniondefsum_values(a: Union[int, float], b: Union[int, float]) ->Union[int, float]:
# a and b can be either int or floatreturna+b

Starting Python 3.10:

defsum_values(a: int|float, b: int|float) ->int|float:
returna+b

Type Hinting & Containers

Before Python 3.10:

fromtypingimportDict, Listdefmake_list(a: str, b: str) ->List[str]:
# return type is a list of stringsreturn [a, b]
defmake_dict(k: str, v: str) ->Dict[str, str]:
# return type is a dict with string keys and string valuesreturn {k: v}
make_list("hello", "world")
>>> ['hello', 'world']
make_dict("hello", "world")
>>> {'hello': 'world'}

Starting Python 3.10:

defmake_list(a: str, b: str) ->list[str]:
return [a, b]
defmake_dict(k: str, v: str) ->dict[str, str]:
return {k: v}

Type Hinting & Generic Objects

Before Python 3.10:

fromdataclassesimportdataclassfromtypingimportList@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: List[City]) ->List[City]:
returnsorted(cities, key=lambdax: x.name)
cities= [
City(name='Madrid', country='ES'),
City(name='Berlin', country='DE'),
City(name='Edinburgh', country='UK')
]
sort_cities(cities)
>>> [City(name='Berlin', country='DE'), City(name='Edinburgh', country='UK'), City(name='Madrid', country='ES')]

Starting Python 3.10:

fromdataclassesimportdataclass@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: list[City]) ->list[City]:
returnsorted(cities, key=lambdax: x.name)

Type Hinting and Local Variables

Not only limited to inputs/outputs of a function:

Before Python 3.10:

fromtypingimportListresult: List[str] = [] # not just a list of anything!

Starting Python 3.10:

result: list[str] = []

👷‍♂️ Operators


👯‍♀️ Double Comparison

🚫

ifvalue>0andvalue<100:

if0<value<100:

🧬 Pattern Matching

Added in Python 3.10. Similar to switch statements in other languages, on steroids:

defparse_command(command: str) ->str:
matchcommand.split():
case [action, direction]:
returnf"Parsed: {action=}, {direction=}"case ["help"]: return"Help message goes here"case _:
return"Wrong command, 2 words expected"parse_command("go north")
>>>"Parsed: action='go', direction='north'"parse_command("look up")
>>>"Parsed: action='look', direction='up'"parse_command("go")
>>>"Wrong command, 2 words expected"parse_command("help")
>>>"Help message goes here"

Alias matching with as, OR matching with | and conditional matching:

defparse_command(command: str) ->str:
matchcommand.split():
case ["go", ("north"|"south") asdirection]:
returnf"Going {direction}"case ["go", _]: return"Sorry, can't go there!"case (["pick", obj, "up"] | ["pick", "up", obj]) ifobjin ['shovel', 'rock']:
returnf"Picking up {obj}"case ["pick", _, "up"] | ["pick", "up", _]:
return"Sorry, can't pick this up!"case _:
return"Wrong command, 2 words expected"parse_command("go south")
>>>"Going south"parse_command("go left")
>>>"Sorry, can't go there!"parse_command("pick shovel up")
>>>"Picking up shovel"parse_command("pick phone up")
>>>"Sorry, can't pick this up!"

Adapting to different structure types:

fromdataclassesimportdataclassfromdatetimeimportdatetime@dataclassclassUser:
age: intdefget_age(user: dict|User) ->int:
matchuser:
caseUser(age):
returnagecase {"dob": {"age": int(age) |float(age)}}:
returnint(age)
case {"dob": dob}:
now=datetime.now()
dob_date=datetime.strptime(dob, "%Y-%m-%d %H:%M:%S")
returnnow.year-dob_date.year
get_age({"dob": "1966-04-17 11:57:01"})
>>>56get_age({"dob": {"date": "1957-05-20T08:36:09.083Z", "age": 64}})
>>>64get_age({"dob": {"age": 39.6}})
>>>39get_age(User(age=40))
>>>40

🍴 Ternary Operator

ifa==5:
result="Five!"else:
result="Not five..."

Shorter way to write the same:

result="Five!"ifa==5else"Not five..."

🦷 Walrus Operator :=

Added in Python 3.8:

value=123print(value)
>>>123# can be written as:print(value:=123)
>>>123value>>>123

😒 Can be fine, but not the most concise way:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
# get some stats on the list: length, sum, mean valuesnum_length=len(numbers)
num_sum=sum(numbers)
stats= {
"length": num_length,
"sum": num_sum,
"mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

✅ Doing the same with less lines of code:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
stats= {
"length": (num_length:=len(numbers)), "sum": (num_sum:=sum(numbers)), "mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

🛂 == vs is

  • ==: do two objects have the same contents?
  • is: are two objects the same thing (point to the same address in memory)?
a= [1, 2, 3]
b= [1, 2, 3]
a==b>>>True
id(a) # Python id of object a>>>4435362944id(b) # Python id of object b>>>4435377344aisb# same as id(a) == id(b)>>>Falsea=baisb>>>True

ℹ️ As a consequence:

a= [1, 2, 3]
b= [1, 2, 3]
a[0] =4print(a, b)
>>> [4, 2, 3] [1, 2, 3]
a=b# make a point to the same object as b, not copying contents of b!a[0] =4# also changes b now as a and b point to the same address in memoryprint(a, b)
>>> [4, 2, 3] [4, 2, 3]

Copying an Object

🚫 Looks cryptic, and only works for lists but not for e.g. dicts:

a= [1, 2, 3]
b=a[:]
a==b>>>Trueaisb>>>False

a= [1, 2, 3]
b=a.copy()
a==b>>>Trueaisb>>>False

ℹ️ There's only one global None object

c=Noned=Nonec==d>>>Truecisd# c and d and not "copies" of None, they point to it>>>True

🧮 Named Parameters

defprint_issue_info(issue_id: str, issue_title: str)
print(f"Issue id: {issue_id}, title: {issue_title}")

😒 Can be fine:

print_issue_info("1234", "Create new thing")
>>>"Issue id: 1234, title: Create new thing"

✅ More explicit and human-readable:

print_issue_info(issue_id="1234", issue_title="Create new thing")
>>>"Issue id: 1234, title: Create new thing"print_issue_info(issue_title="Create new thing", issue_id="1234")
>>>"Issue id: 1234, title: Create new thing"

🏀 Practical Examples


📁 Reading/Writing Files

🚫 Handle errors yourself:

f=open('data.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()

✅ Use a context managerwith:

withopen("data.txt", "r") asf:
data=f.read()
withopen("data2.txt", "w") asf:
f.write(data)

🅾️ Loading/Exporting JSON

json is a built-in Python module:

importjsondata= {"a": 123, "b": None}
data_json=json.dumps(data) # dumps = dump stringprint(data_json)
>>> {"a": 123, "b": null}
data_parsed=json.loads(data_json) # loads = load stringprint(data_parsed==data)
>>>True

Reading JSON Files

$ cat file.json
{
"a": {"b": 123}
}
importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123

🌍 HTTP Requests

✅ Use the requests library:

importrequestsurl='https://api.github.com/some/endpoint'headers= {'Authentication': 'Bearer mytoken'}
r=requests.get(url, headers=headers)
print(r.status_code)
>>>200print(r.json())
>>> {"status": "OK", "message": "hi from the API"}

🔨 Tools


🔁 REPL

REPL = read-eval-print loop

Can be used to quickly try things out in a terminal:

$ python3
Python 3.10.6 (main, Aug 11 2022, 13:49:25) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license"for more information.
>>> sum(range(0, 10))
45

🐞 Debugging

breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):

value=123breakpoint()
(Pdb) value>>>123

🏁 Outro: Key Advice

  • Write as short and lean code as possible
  • Use the most recent version of Python
  • Try ideas with REPL quickly
  • Use type hinting
  • Know your basic data structures
  • Use list & dict comprehensions
  • Use f-strings
  • Use data classes
  • RealPython.com is a great source of guide on specific topics (example)

🙇‍♂️ Thank You!

About

Slides for tech talk in Camunda on Python tips and tricks (Sep 2022)

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🐍 Python: Tips & Tricks

How to Write More Pythonic Code

📽 Recording


Table of Contents


🪷 Zen of Python

importthis
>>> The Zen of Python, by Tim Peters
>>> Beautiful is better than ugly.
>>> Explicit is better than implicit.
>>> Simple is better than complex.
>>> Complex is better than complicated.
>>> Flat is better than nested.
>>> Sparse is better than dense.
>>> Readability counts.
>>> Special cases aren't special enough to break the rules.
>>> Although practicality beats purity.
>>> Errors should never pass silently.
>>> Unless explicitly silenced.
>>> In the face of ambiguity, refuse the temptation to guess.
>>> There should be one-- and preferably only one --obvious way to do it.
>>> Although that way may not be obvious at first unless you're Dutch.
>>> Now is better than never.
>>> Although never is often better than *right* now.
>>> If the implementation is hard to explain, it's a bad idea.
>>> If the implementation is easy to explain, it may be a good idea.
>>> Namespaces are one honking great idea -- let's do more of those!

📝 Python History

  • 3.6 (Dec 23, 2016): f-strings
  • 3.7 (Jun 27, 2018): data classes
  • 3.8 (Oct 14, 2019): walrus operator
  • 3.9 (Oct 5, 2020): simpler dictionary updates/merges
  • 3.10 (Oct 4, 2021): pattern matching
  • 3.11 (Oct 3, 2022): performance increases (~25%)

🎻 String Formatting

There are 4 ways to do that in Python, with a single preferred one.


The Simple (+)

🚫 Plain approach to glue strings together:

name="Bob"greeting="hello"message=greeting+" there, "+name+"!"message>>>'hello there, Bob!'

ℹ️ One can only concatenate strings with strings:

units=10items="apples"print("Currently in stock: "+str(units) +" "+items)
>>>'Currently in stock: 10 apples'

The Old (%)

🚫

name="Bob"greeting="hello"message="%s there, %s!"% (greeting, name)
message>>>'hello there, Bob!'# same result:message="%(greeting)s, %(name)s!"% {"greeting": greeting, "name": name}

The Meh (.format)

🚫

name="Bob"greeting="hello"message="{} there, {}!".format(greeting, name)
message>>>'hello there, Bob!'# can enumerate params:message="{0} there, {1}!".format(greeting, name)
message="{1} there, {0}!".format(name, greeting)
>>>'hello there, Bob!'# same result:message="{greeting} there, {name}!".format(greeting=greeting, name=name)

The Good (f-strings)

Added in Python 3.6:

✅ Looks better, more powerful, better performance (2x faster than format, 50% faster than %):

name="Bob"greeting="hello"message=f"{greeting} there, {name}!"message>>>'hello there, Bob!'

ℹ️ Any Python expressions and value formatting support:

importmathr=2print(f"Circle of radius {r} has a circumference of {2*math.pi*r}")
>>>Circleofradius2hasacircumferenceof12.566370614359172print(f"Circle of radius {r} has a circumference of {2*math.pi*r:.2f}")
>>>Circleofradius2hasacircumferenceof12.57

ℹ️ Debugging specifier = (added in Python 3.8):

x=123; y=456print(f"Calculated values: x={x}, y={y}")
>>>Calculatedvalues: x=123, y=456print(f"Calculated values: {x=}, {y=}")
>>>Calculatedvalues: x=123, y=456data= {'city': 'Berlin', 'country': 'DE'}
print(f"Result: {data=}")
>>>Result: data={'city': 'Berlin', 'country': 'DE'}

🌱 Basic Data Structures


  • Lists
  • Strings
  • Dicts
  • Tuples
  • Sets

Lists


A list is a mutable, ordered array of values

data= [1, 3, 5]
data.append(7)
data>>> [1, 3, 5, 7]
data.extend([9, 11]) # same as: data += [9, 11]>>> [1, 3, 5, 7, 9, 11]
len(data)
>>>6

Iteration

🚫 Index-based iteration loops:

foriinrange(len(data)):
print(data[i])
>>>1>>>3>>>5

✅ Every list is iterable:

forxindata:
print(x)
>>>1>>>3>>>5

ℹ️ In case one needs to access the current element's index:

fori, xinenumerate(data):
print(f"Element {i}: {x}")
>>>Element0: 1>>>Element1: 3>>>Element2: 5

Math Operations

🚫

data= [1, 2, -3, 4, 5]
sum_=0min_=data[0]
max_=data[0]
forxindata:
sum_+=xifx<min_:
min_=xifx>max_:
max_=xsum_>>>9min_>>>-3max_>>>5

data= [1, 2, -3, 4, 5]
sum(data)
>>>9min(data)
>>>-3max(data)
>>>5

List Slicing [::]

Done with so-called 🍣 sushi-operator ([::]):

array[<start_index>:<stop_index>:<step>]
  • start_index = 0 if not specified
  • stop_index = len(array) if not specified
    • it's exclusive: stop_index value is not included in the slice result
  • step_index = 1 if not specified

data= [2, 4, 6, 8, 10]
# index: 0 1 2 3 4data[1:] # same as [1::] or [1:5:1]>>> [4, 6, 8, 10]
data[1:3] # same as [1:3:1]>>> [4, 6]
data[::2] # same as [0:5:2]>>> [2, 6, 10]
data==data[0:5:1]
>>>Truedata==data[:]
>>>True

Slicing the full list with step -1 (backwards) returns a reversed version of the list:

data= [2, 4, 6, 8, 10]
data[::-1]
>>> [10, 8, 6, 4, 2]

Membership Testing With in

🚫 Implement searching algorithm yourself:

array= [1, 2, 3, 4, 5]
search_for=3found=Falseforiinrange(len(array)):
ifarray[i] ==search_for:
found=Truebreakprint(f"Found: {found}")
>>>True

✅ Let Python do it:

array= [1, 2, 3, 4, 5]
search_for=3found=search_forinarrayprint(f"Found: {found}")
>>>True

List Comprehension

Formula: [value for item in iterable] (for every item in iterable map it to value)

# range(A, B, C) = iterator of integer sequence from A to B with a step C (B is excluded) data= [x**2forxinrange(0, 5)]
data>>> [0, 1, 4, 9, 16]

List comprehension with a condition (formula: [value for item in iterable if condition])

data= [3, 2, -5, 10, 21, 7]
even= [xforxindataifx%2==0]
even>>> [2, 10]

Mapping and Filtering

Alternative to list comprehension is to use map (with a lambda function (inline function))

data=map(lambdax: x**2, range(0, 5))
print(list(data)) # `map` returns an iterator, `list` creates a materialized list of it>>> [0, 1, 4, 9, 16]

Alternative to list comprehension with a condition is to use filter (with a lambda function (inline function)):

data= [3, 2, -5, 10, 21, 7]
even=filter(lambdax: x%2==0, data)
list(even) # `filter` returns an iterator, `list` creates a materialized list of it>>> [2, 10]

Sorting Lists

data= [
{'city': 'Paris', 'country': 'FR'},
{'city': 'Berlin', 'country': 'DE'},
{'city': 'London', 'country': 'UK'}
]
# order by city name:sorted(data, key=lambdax: x['city'])
>>> [{'city': 'Berlin', 'country': 'DE'}, {'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}]
# order by country code reversed:sorted(data, key=lambdax: x['country'], reverse=True)
>>> [{'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}, {'city': 'Berlin', 'country': 'DE'}]

Truthiness

🚫 Check if the list is empty/not empty:

iflen(data) ==0:
print("List is empty")
iflen(data) >0:
print("List is not empty")

ifnotdata:
print("List is empty")
ifdata:
print("List is not empty")

Flatten List of lists

regular_list= [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
flat_list= [itemforsublistinregular_listforiteminsublist]
print('Original list', regular_list)
>>>Originallist [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
print('Transformed list', flat_list)
>>>Transformedlist [1, 2, 3, 4, 5, 6, 7, 8, 9]

Strings


A string can be seen as an iterable list of characters:

data='oslo'forletterindata:
print(letter.upper())
>>>O>>>S>>>L>>>Odata[2]
>>>'l'len(data)
>>>4data[::-1]
>>>'olso'
[ord(x) forxindata] # ord(x) == Unicode integer of character x>>> [111, 115, 108, 111]

Membership Check With in

🚫 Is substring in string:

"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False

"aura"in"restaurant">>>True"fun"in"waterfall">>>False

Stripping Whitespace Characters

data=' empty spaces, what are we living for? 'print(data.strip())
>>>'empty spaces, what are we living for?'print(data.rstrip())
>>>' empty spaces, what are we living for?'print(data.lstrip())
>>>'empty spaces, what are we living for? '

Prefix/Suffix Manipulations

Added in Python 3.9:

print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'

Tokenization

string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']

Dicts


Dict is key-val storage:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'

Iteration

Any dict is iterable:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
# iterate other keysforkeyindata:
print(f"{key}: {data[key]}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']
# iterate over keys with values:forkey, valindata.items():
print(f"{key}: {val}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']

Membership Check With in

🚫

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
found=Falseforkeyindata:
ifkey=='city':
found=Truebreakfound>>>True

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
'city'indata>>>True'population'indata>>>False'continent'notindata>>>True

Dict Comprehension

Formula: {key: val for item in iterable}

data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}

Accessing Values: [] vs. get

data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexception

data= {'a': 123, 'b': 456}
data.get('a')
>>>123data.get('b')
>>>456data.get('c')
>>>Nonedata.get('c', 'default value')
>>>'default value'

FYI, there's no set for dicts, values to be changes with the [] notation only (e.g. data["key"] = "val")


Safe Navigation

🚫 Error-prone code:

data= {'a': {'b': {'c': 123}}}
element=data['a']['b']['c']
element>>>123data= {'a': {'d': 456}}
element=data['a']['b']['c']
>>>KeyError!

✅ Robust way to inspect a dictionary:

data= {'a': {'d': 456}}
element=data.get('a', {}).get('b', {}).get('c')
element>>>None

Merging

europe= {'Madrid': 'Spain', 'Rome': 'Italy'}
asia= {'Tokyo': 'Japan', 'Manila': 'Philippines'}

Before Python 3.9:

{**europe, **asia}
>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Starting Python 3.9:

europe|asia>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Tuples


Tuple is an immutable, ordered array of values:

data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignment

Tuples can be implicit:

a=1, 2a[0]
>>>1# the same is:a= (1, 2)

Sets


Set is a unordered array of unique values:

data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptable

🧑‍🔧 Data Structures: Examples


🦄 Get Unique Elements of a List

Converting a list to a set removes duplicates:

data= [5, 2, 3, 2, 4, 3, 1]
unique=list(set(data))
unique>>> [1, 2, 3, 4, 5]

🔍 Search in a List of Objects

data= [
{"city": "Berlin", "country": "DE"},
{"city": "Sydney", "country": "AU"},
{"city": "Stockholm", "country": "SE"}
]
search=next((itemforitemindataifitem["city"] =="Sydney"), None)
search["country"]
>>>'AU'search=next((itemforitemindataifitem["city"] =="Paris"), None)
searchisNone>>>True

🪡 Is Any of the Values

🚫

ifoperation=="READ"oroperation=="WRITE":

ifoperationin ["READ", "WRITE"]:

📦 Values Unpacking


🔁 Variable Swapping

🚫 Using a temporary variable:

a=5; b=4tmp=aa=bb=tmpa>>>4b>>>5

✅ Cut to the chase:

a=5; b=4a, b=b, a# same as a, b = (b, a)a>>>4b>>>5

📭 Unpacking With Lists and Tuples

🚫

data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two

data= ['one', 'two']
a, b=dataa>>>oneb>>>two

Also works for tuples:

data= ('one', 'two', 'many', 'things')
a, b, *c=datac>>> ['many', 'things']
a, b=123, 456# same as a, b = (123, 456)a>>>123b>>>456

🌾 Data Classes


🚫 Describing objects can be done with dicts:

d1= {'name': 'Moabit', 'city': 'Berlin', 'country': 'DE', 'area': 7.72}
d2= {'name': 'Greenwich', 'city': 'London', 'country': 'UK', 'area': 47.3}
d2['city']
>>>'London'

Problem: no type hinting possible (e.g. that name is str and area should be a float) and there are no object structure restrictions in place.


🚫 We can use classes for solving it but that's bit too verbose:

classDistrict:
def__init__(self, name: str, city: str, country: str, area: float):
self.name: str=nameself.city: str=cityself.country: str=countryself.area: str=aread1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d2.city>>>'London'

✅ Using data classes (added in Python 3.7):

fromdataclassesimportdataclass@dataclassclassDistrict:
name: strcity: strcountry: strarea: float=0.0d1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d3=District(name='Brooklyn', city='New York', country='US')
d2.city>>>'London'd3.area>>>0.0

🦆 Type Hinting

There's no run-time type checking, but code with type hints allows:

  • IDEs (e.g. PyCharm) and static type checkers (e.g. mypy) to catch errors before runtime
  • to have a better, self-documented code

Basic Type Hinting

🚫

defsum_values(a, b):
returna+bsum_values(10, 3)
>>>13sum_values(10, "x")
>>>TypeError: unsupportedoperand type(s) for+: 'int'and'str'

defsum_values(a: int, b: int) ->int:
returna+bsum_values(10, "x") # IDE will highlight an error

Type Hinting & Multiple Types

Before Python 3.10:

fromtypingimportUniondefsum_values(a: Union[int, float], b: Union[int, float]) ->Union[int, float]:
# a and b can be either int or floatreturna+b

Starting Python 3.10:

defsum_values(a: int|float, b: int|float) ->int|float:
returna+b

Type Hinting & Containers

Before Python 3.10:

fromtypingimportDict, Listdefmake_list(a: str, b: str) ->List[str]:
# return type is a list of stringsreturn [a, b]
defmake_dict(k: str, v: str) ->Dict[str, str]:
# return type is a dict with string keys and string valuesreturn {k: v}
make_list("hello", "world")
>>> ['hello', 'world']
make_dict("hello", "world")
>>> {'hello': 'world'}

Starting Python 3.10:

defmake_list(a: str, b: str) ->list[str]:
return [a, b]
defmake_dict(k: str, v: str) ->dict[str, str]:
return {k: v}

Type Hinting & Generic Objects

Before Python 3.10:

fromdataclassesimportdataclassfromtypingimportList@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: List[City]) ->List[City]:
returnsorted(cities, key=lambdax: x.name)
cities= [
City(name='Madrid', country='ES'),
City(name='Berlin', country='DE'),
City(name='Edinburgh', country='UK')
]
sort_cities(cities)
>>> [City(name='Berlin', country='DE'), City(name='Edinburgh', country='UK'), City(name='Madrid', country='ES')]

Starting Python 3.10:

fromdataclassesimportdataclass@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: list[City]) ->list[City]:
returnsorted(cities, key=lambdax: x.name)

Type Hinting and Local Variables

Not only limited to inputs/outputs of a function:

Before Python 3.10:

fromtypingimportListresult: List[str] = [] # not just a list of anything!

Starting Python 3.10:

result: list[str] = []

👷‍♂️ Operators


👯‍♀️ Double Comparison

🚫

ifvalue>0andvalue<100:

if0<value<100:

🧬 Pattern Matching

Added in Python 3.10. Similar to switch statements in other languages, on steroids:

defparse_command(command: str) ->str:
matchcommand.split():
case [action, direction]:
returnf"Parsed: {action=}, {direction=}"case ["help"]: return"Help message goes here"case _:
return"Wrong command, 2 words expected"parse_command("go north")
>>>"Parsed: action='go', direction='north'"parse_command("look up")
>>>"Parsed: action='look', direction='up'"parse_command("go")
>>>"Wrong command, 2 words expected"parse_command("help")
>>>"Help message goes here"

Alias matching with as, OR matching with | and conditional matching:

defparse_command(command: str) ->str:
matchcommand.split():
case ["go", ("north"|"south") asdirection]:
returnf"Going {direction}"case ["go", _]: return"Sorry, can't go there!"case (["pick", obj, "up"] | ["pick", "up", obj]) ifobjin ['shovel', 'rock']:
returnf"Picking up {obj}"case ["pick", _, "up"] | ["pick", "up", _]:
return"Sorry, can't pick this up!"case _:
return"Wrong command, 2 words expected"parse_command("go south")
>>>"Going south"parse_command("go left")
>>>"Sorry, can't go there!"parse_command("pick shovel up")
>>>"Picking up shovel"parse_command("pick phone up")
>>>"Sorry, can't pick this up!"

Adapting to different structure types:

fromdataclassesimportdataclassfromdatetimeimportdatetime@dataclassclassUser:
age: intdefget_age(user: dict|User) ->int:
matchuser:
caseUser(age):
returnagecase {"dob": {"age": int(age) |float(age)}}:
returnint(age)
case {"dob": dob}:
now=datetime.now()
dob_date=datetime.strptime(dob, "%Y-%m-%d %H:%M:%S")
returnnow.year-dob_date.year
get_age({"dob": "1966-04-17 11:57:01"})
>>>56get_age({"dob": {"date": "1957-05-20T08:36:09.083Z", "age": 64}})
>>>64get_age({"dob": {"age": 39.6}})
>>>39get_age(User(age=40))
>>>40

🍴 Ternary Operator

ifa==5:
result="Five!"else:
result="Not five..."

Shorter way to write the same:

result="Five!"ifa==5else"Not five..."

🦷 Walrus Operator :=

Added in Python 3.8:

value=123print(value)
>>>123# can be written as:print(value:=123)
>>>123value>>>123

😒 Can be fine, but not the most concise way:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
# get some stats on the list: length, sum, mean valuesnum_length=len(numbers)
num_sum=sum(numbers)
stats= {
"length": num_length,
"sum": num_sum,
"mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

✅ Doing the same with less lines of code:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
stats= {
"length": (num_length:=len(numbers)), "sum": (num_sum:=sum(numbers)), "mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

🛂 == vs is

  • ==: do two objects have the same contents?
  • is: are two objects the same thing (point to the same address in memory)?
a= [1, 2, 3]
b= [1, 2, 3]
a==b>>>True
id(a) # Python id of object a>>>4435362944id(b) # Python id of object b>>>4435377344aisb# same as id(a) == id(b)>>>Falsea=baisb>>>True

ℹ️ As a consequence:

a= [1, 2, 3]
b= [1, 2, 3]
a[0] =4print(a, b)
>>> [4, 2, 3] [1, 2, 3]
a=b# make a point to the same object as b, not copying contents of b!a[0] =4# also changes b now as a and b point to the same address in memoryprint(a, b)
>>> [4, 2, 3] [4, 2, 3]

Copying an Object

🚫 Looks cryptic, and only works for lists but not for e.g. dicts:

a= [1, 2, 3]
b=a[:]
a==b>>>Trueaisb>>>False

a= [1, 2, 3]
b=a.copy()
a==b>>>Trueaisb>>>False

ℹ️ There's only one global None object

c=Noned=Nonec==d>>>Truecisd# c and d and not "copies" of None, they point to it>>>True

🧮 Named Parameters

defprint_issue_info(issue_id: str, issue_title: str)
print(f"Issue id: {issue_id}, title: {issue_title}")

😒 Can be fine:

print_issue_info("1234", "Create new thing")
>>>"Issue id: 1234, title: Create new thing"

✅ More explicit and human-readable:

print_issue_info(issue_id="1234", issue_title="Create new thing")
>>>"Issue id: 1234, title: Create new thing"print_issue_info(issue_title="Create new thing", issue_id="1234")
>>>"Issue id: 1234, title: Create new thing"

🏀 Practical Examples


📁 Reading/Writing Files

🚫 Handle errors yourself:

f=open('data.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()

✅ Use a context managerwith:

withopen("data.txt", "r") asf:
data=f.read()
withopen("data2.txt", "w") asf:
f.write(data)

🅾️ Loading/Exporting JSON

json is a built-in Python module:

importjsondata= {"a": 123, "b": None}
data_json=json.dumps(data) # dumps = dump stringprint(data_json)
>>> {"a": 123, "b": null}
data_parsed=json.loads(data_json) # loads = load stringprint(data_parsed==data)
>>>True

Reading JSON Files

$ cat file.json
{
"a": {"b": 123}
}
importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123

🌍 HTTP Requests

✅ Use the requests library:

importrequestsurl='https://api.github.com/some/endpoint'headers= {'Authentication': 'Bearer mytoken'}
r=requests.get(url, headers=headers)
print(r.status_code)
>>>200print(r.json())
>>> {"status": "OK", "message": "hi from the API"}

🔨 Tools


🔁 REPL

REPL = read-eval-print loop

Can be used to quickly try things out in a terminal:

$ python3
Python 3.10.6 (main, Aug 11 2022, 13:49:25) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license"for more information.
>>> sum(range(0, 10))
45

🐞 Debugging

breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):

value=123breakpoint()
(Pdb) value>>>123

🏁 Outro: Key Advice

  • Write as short and lean code as possible
  • Use the most recent version of Python
  • Try ideas with REPL quickly
  • Use type hinting
  • Know your basic data structures
  • Use list & dict comprehensions
  • Use f-strings
  • Use data classes
  • RealPython.com is a great source of guide on specific topics (example)

🙇‍♂️ Thank You!

About

Slides for tech talk in Camunda on Python tips and tricks (Sep 2022)

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🐍 Python: Tips & Tricks

How to Write More Pythonic Code

📽 Recording


Table of Contents


🪷 Zen of Python

importthis
>>> The Zen of Python, by Tim Peters
>>> Beautiful is better than ugly.
>>> Explicit is better than implicit.
>>> Simple is better than complex.
>>> Complex is better than complicated.
>>> Flat is better than nested.
>>> Sparse is better than dense.
>>> Readability counts.
>>> Special cases aren't special enough to break the rules.
>>> Although practicality beats purity.
>>> Errors should never pass silently.
>>> Unless explicitly silenced.
>>> In the face of ambiguity, refuse the temptation to guess.
>>> There should be one-- and preferably only one --obvious way to do it.
>>> Although that way may not be obvious at first unless you're Dutch.
>>> Now is better than never.
>>> Although never is often better than *right* now.
>>> If the implementation is hard to explain, it's a bad idea.
>>> If the implementation is easy to explain, it may be a good idea.
>>> Namespaces are one honking great idea -- let's do more of those!

📝 Python History

  • 3.6 (Dec 23, 2016): f-strings
  • 3.7 (Jun 27, 2018): data classes
  • 3.8 (Oct 14, 2019): walrus operator
  • 3.9 (Oct 5, 2020): simpler dictionary updates/merges
  • 3.10 (Oct 4, 2021): pattern matching
  • 3.11 (Oct 3, 2022): performance increases (~25%)

🎻 String Formatting

There are 4 ways to do that in Python, with a single preferred one.


The Simple (+)

🚫 Plain approach to glue strings together:

name="Bob"greeting="hello"message=greeting+" there, "+name+"!"message>>>'hello there, Bob!'

ℹ️ One can only concatenate strings with strings:

units=10items="apples"print("Currently in stock: "+str(units) +" "+items)
>>>'Currently in stock: 10 apples'

The Old (%)

🚫

name="Bob"greeting="hello"message="%s there, %s!"% (greeting, name)
message>>>'hello there, Bob!'# same result:message="%(greeting)s, %(name)s!"% {"greeting": greeting, "name": name}

The Meh (.format)

🚫

name="Bob"greeting="hello"message="{} there, {}!".format(greeting, name)
message>>>'hello there, Bob!'# can enumerate params:message="{0} there, {1}!".format(greeting, name)
message="{1} there, {0}!".format(name, greeting)
>>>'hello there, Bob!'# same result:message="{greeting} there, {name}!".format(greeting=greeting, name=name)

The Good (f-strings)

Added in Python 3.6:

✅ Looks better, more powerful, better performance (2x faster than format, 50% faster than %):

name="Bob"greeting="hello"message=f"{greeting} there, {name}!"message>>>'hello there, Bob!'

ℹ️ Any Python expressions and value formatting support:

importmathr=2print(f"Circle of radius {r} has a circumference of {2*math.pi*r}")
>>>Circleofradius2hasacircumferenceof12.566370614359172print(f"Circle of radius {r} has a circumference of {2*math.pi*r:.2f}")
>>>Circleofradius2hasacircumferenceof12.57

ℹ️ Debugging specifier = (added in Python 3.8):

x=123; y=456print(f"Calculated values: x={x}, y={y}")
>>>Calculatedvalues: x=123, y=456print(f"Calculated values: {x=}, {y=}")
>>>Calculatedvalues: x=123, y=456data= {'city': 'Berlin', 'country': 'DE'}
print(f"Result: {data=}")
>>>Result: data={'city': 'Berlin', 'country': 'DE'}

🌱 Basic Data Structures


  • Lists
  • Strings
  • Dicts
  • Tuples
  • Sets

Lists


A list is a mutable, ordered array of values

data= [1, 3, 5]
data.append(7)
data>>> [1, 3, 5, 7]
data.extend([9, 11]) # same as: data += [9, 11]>>> [1, 3, 5, 7, 9, 11]
len(data)
>>>6

Iteration

🚫 Index-based iteration loops:

foriinrange(len(data)):
print(data[i])
>>>1>>>3>>>5

✅ Every list is iterable:

forxindata:
print(x)
>>>1>>>3>>>5

ℹ️ In case one needs to access the current element's index:

fori, xinenumerate(data):
print(f"Element {i}: {x}")
>>>Element0: 1>>>Element1: 3>>>Element2: 5

Math Operations

🚫

data= [1, 2, -3, 4, 5]
sum_=0min_=data[0]
max_=data[0]
forxindata:
sum_+=xifx<min_:
min_=xifx>max_:
max_=xsum_>>>9min_>>>-3max_>>>5

data= [1, 2, -3, 4, 5]
sum(data)
>>>9min(data)
>>>-3max(data)
>>>5

List Slicing [::]

Done with so-called 🍣 sushi-operator ([::]):

array[<start_index>:<stop_index>:<step>]
  • start_index = 0 if not specified
  • stop_index = len(array) if not specified
    • it's exclusive: stop_index value is not included in the slice result
  • step_index = 1 if not specified

data= [2, 4, 6, 8, 10]
# index: 0 1 2 3 4data[1:] # same as [1::] or [1:5:1]>>> [4, 6, 8, 10]
data[1:3] # same as [1:3:1]>>> [4, 6]
data[::2] # same as [0:5:2]>>> [2, 6, 10]
data==data[0:5:1]
>>>Truedata==data[:]
>>>True

Slicing the full list with step -1 (backwards) returns a reversed version of the list:

data= [2, 4, 6, 8, 10]
data[::-1]
>>> [10, 8, 6, 4, 2]

Membership Testing With in

🚫 Implement searching algorithm yourself:

array= [1, 2, 3, 4, 5]
search_for=3found=Falseforiinrange(len(array)):
ifarray[i] ==search_for:
found=Truebreakprint(f"Found: {found}")
>>>True

✅ Let Python do it:

array= [1, 2, 3, 4, 5]
search_for=3found=search_forinarrayprint(f"Found: {found}")
>>>True

List Comprehension

Formula: [value for item in iterable] (for every item in iterable map it to value)

# range(A, B, C) = iterator of integer sequence from A to B with a step C (B is excluded) data= [x**2forxinrange(0, 5)]
data>>> [0, 1, 4, 9, 16]

List comprehension with a condition (formula: [value for item in iterable if condition])

data= [3, 2, -5, 10, 21, 7]
even= [xforxindataifx%2==0]
even>>> [2, 10]

Mapping and Filtering

Alternative to list comprehension is to use map (with a lambda function (inline function))

data=map(lambdax: x**2, range(0, 5))
print(list(data)) # `map` returns an iterator, `list` creates a materialized list of it>>> [0, 1, 4, 9, 16]

Alternative to list comprehension with a condition is to use filter (with a lambda function (inline function)):

data= [3, 2, -5, 10, 21, 7]
even=filter(lambdax: x%2==0, data)
list(even) # `filter` returns an iterator, `list` creates a materialized list of it>>> [2, 10]

Sorting Lists

data= [
{'city': 'Paris', 'country': 'FR'},
{'city': 'Berlin', 'country': 'DE'},
{'city': 'London', 'country': 'UK'}
]
# order by city name:sorted(data, key=lambdax: x['city'])
>>> [{'city': 'Berlin', 'country': 'DE'}, {'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}]
# order by country code reversed:sorted(data, key=lambdax: x['country'], reverse=True)
>>> [{'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}, {'city': 'Berlin', 'country': 'DE'}]

Truthiness

🚫 Check if the list is empty/not empty:

iflen(data) ==0:
print("List is empty")
iflen(data) >0:
print("List is not empty")

ifnotdata:
print("List is empty")
ifdata:
print("List is not empty")

Flatten List of lists

regular_list= [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
flat_list= [itemforsublistinregular_listforiteminsublist]
print('Original list', regular_list)
>>>Originallist [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
print('Transformed list', flat_list)
>>>Transformedlist [1, 2, 3, 4, 5, 6, 7, 8, 9]

Strings


A string can be seen as an iterable list of characters:

data='oslo'forletterindata:
print(letter.upper())
>>>O>>>S>>>L>>>Odata[2]
>>>'l'len(data)
>>>4data[::-1]
>>>'olso'
[ord(x) forxindata] # ord(x) == Unicode integer of character x>>> [111, 115, 108, 111]

Membership Check With in

🚫 Is substring in string:

"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False

"aura"in"restaurant">>>True"fun"in"waterfall">>>False

Stripping Whitespace Characters

data=' empty spaces, what are we living for? 'print(data.strip())
>>>'empty spaces, what are we living for?'print(data.rstrip())
>>>' empty spaces, what are we living for?'print(data.lstrip())
>>>'empty spaces, what are we living for? '

Prefix/Suffix Manipulations

Added in Python 3.9:

print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'

Tokenization

string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']

Dicts


Dict is key-val storage:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'

Iteration

Any dict is iterable:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
# iterate other keysforkeyindata:
print(f"{key}: {data[key]}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']
# iterate over keys with values:forkey, valindata.items():
print(f"{key}: {val}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']

Membership Check With in

🚫

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
found=Falseforkeyindata:
ifkey=='city':
found=Truebreakfound>>>True

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
'city'indata>>>True'population'indata>>>False'continent'notindata>>>True

Dict Comprehension

Formula: {key: val for item in iterable}

data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}

Accessing Values: [] vs. get

data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexception

data= {'a': 123, 'b': 456}
data.get('a')
>>>123data.get('b')
>>>456data.get('c')
>>>Nonedata.get('c', 'default value')
>>>'default value'

FYI, there's no set for dicts, values to be changes with the [] notation only (e.g. data["key"] = "val")


Safe Navigation

🚫 Error-prone code:

data= {'a': {'b': {'c': 123}}}
element=data['a']['b']['c']
element>>>123data= {'a': {'d': 456}}
element=data['a']['b']['c']
>>>KeyError!

✅ Robust way to inspect a dictionary:

data= {'a': {'d': 456}}
element=data.get('a', {}).get('b', {}).get('c')
element>>>None

Merging

europe= {'Madrid': 'Spain', 'Rome': 'Italy'}
asia= {'Tokyo': 'Japan', 'Manila': 'Philippines'}

Before Python 3.9:

{**europe, **asia}
>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Starting Python 3.9:

europe|asia>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Tuples


Tuple is an immutable, ordered array of values:

data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignment

Tuples can be implicit:

a=1, 2a[0]
>>>1# the same is:a= (1, 2)

Sets


Set is a unordered array of unique values:

data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptable

🧑‍🔧 Data Structures: Examples


🦄 Get Unique Elements of a List

Converting a list to a set removes duplicates:

data= [5, 2, 3, 2, 4, 3, 1]
unique=list(set(data))
unique>>> [1, 2, 3, 4, 5]

🔍 Search in a List of Objects

data= [
{"city": "Berlin", "country": "DE"},
{"city": "Sydney", "country": "AU"},
{"city": "Stockholm", "country": "SE"}
]
search=next((itemforitemindataifitem["city"] =="Sydney"), None)
search["country"]
>>>'AU'search=next((itemforitemindataifitem["city"] =="Paris"), None)
searchisNone>>>True

🪡 Is Any of the Values

🚫

ifoperation=="READ"oroperation=="WRITE":

ifoperationin ["READ", "WRITE"]:

📦 Values Unpacking


🔁 Variable Swapping

🚫 Using a temporary variable:

a=5; b=4tmp=aa=bb=tmpa>>>4b>>>5

✅ Cut to the chase:

a=5; b=4a, b=b, a# same as a, b = (b, a)a>>>4b>>>5

📭 Unpacking With Lists and Tuples

🚫

data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two

data= ['one', 'two']
a, b=dataa>>>oneb>>>two

Also works for tuples:

data= ('one', 'two', 'many', 'things')
a, b, *c=datac>>> ['many', 'things']
a, b=123, 456# same as a, b = (123, 456)a>>>123b>>>456

🌾 Data Classes


🚫 Describing objects can be done with dicts:

d1= {'name': 'Moabit', 'city': 'Berlin', 'country': 'DE', 'area': 7.72}
d2= {'name': 'Greenwich', 'city': 'London', 'country': 'UK', 'area': 47.3}
d2['city']
>>>'London'

Problem: no type hinting possible (e.g. that name is str and area should be a float) and there are no object structure restrictions in place.


🚫 We can use classes for solving it but that's bit too verbose:

classDistrict:
def__init__(self, name: str, city: str, country: str, area: float):
self.name: str=nameself.city: str=cityself.country: str=countryself.area: str=aread1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d2.city>>>'London'

✅ Using data classes (added in Python 3.7):

fromdataclassesimportdataclass@dataclassclassDistrict:
name: strcity: strcountry: strarea: float=0.0d1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d3=District(name='Brooklyn', city='New York', country='US')
d2.city>>>'London'd3.area>>>0.0

🦆 Type Hinting

There's no run-time type checking, but code with type hints allows:

  • IDEs (e.g. PyCharm) and static type checkers (e.g. mypy) to catch errors before runtime
  • to have a better, self-documented code

Basic Type Hinting

🚫

defsum_values(a, b):
returna+bsum_values(10, 3)
>>>13sum_values(10, "x")
>>>TypeError: unsupportedoperand type(s) for+: 'int'and'str'

defsum_values(a: int, b: int) ->int:
returna+bsum_values(10, "x") # IDE will highlight an error

Type Hinting & Multiple Types

Before Python 3.10:

fromtypingimportUniondefsum_values(a: Union[int, float], b: Union[int, float]) ->Union[int, float]:
# a and b can be either int or floatreturna+b

Starting Python 3.10:

defsum_values(a: int|float, b: int|float) ->int|float:
returna+b

Type Hinting & Containers

Before Python 3.10:

fromtypingimportDict, Listdefmake_list(a: str, b: str) ->List[str]:
# return type is a list of stringsreturn [a, b]
defmake_dict(k: str, v: str) ->Dict[str, str]:
# return type is a dict with string keys and string valuesreturn {k: v}
make_list("hello", "world")
>>> ['hello', 'world']
make_dict("hello", "world")
>>> {'hello': 'world'}

Starting Python 3.10:

defmake_list(a: str, b: str) ->list[str]:
return [a, b]
defmake_dict(k: str, v: str) ->dict[str, str]:
return {k: v}

Type Hinting & Generic Objects

Before Python 3.10:

fromdataclassesimportdataclassfromtypingimportList@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: List[City]) ->List[City]:
returnsorted(cities, key=lambdax: x.name)
cities= [
City(name='Madrid', country='ES'),
City(name='Berlin', country='DE'),
City(name='Edinburgh', country='UK')
]
sort_cities(cities)
>>> [City(name='Berlin', country='DE'), City(name='Edinburgh', country='UK'), City(name='Madrid', country='ES')]

Starting Python 3.10:

fromdataclassesimportdataclass@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: list[City]) ->list[City]:
returnsorted(cities, key=lambdax: x.name)

Type Hinting and Local Variables

Not only limited to inputs/outputs of a function:

Before Python 3.10:

fromtypingimportListresult: List[str] = [] # not just a list of anything!

Starting Python 3.10:

result: list[str] = []

👷‍♂️ Operators


👯‍♀️ Double Comparison

🚫

ifvalue>0andvalue<100:

if0<value<100:

🧬 Pattern Matching

Added in Python 3.10. Similar to switch statements in other languages, on steroids:

defparse_command(command: str) ->str:
matchcommand.split():
case [action, direction]:
returnf"Parsed: {action=}, {direction=}"case ["help"]: return"Help message goes here"case _:
return"Wrong command, 2 words expected"parse_command("go north")
>>>"Parsed: action='go', direction='north'"parse_command("look up")
>>>"Parsed: action='look', direction='up'"parse_command("go")
>>>"Wrong command, 2 words expected"parse_command("help")
>>>"Help message goes here"

Alias matching with as, OR matching with | and conditional matching:

defparse_command(command: str) ->str:
matchcommand.split():
case ["go", ("north"|"south") asdirection]:
returnf"Going {direction}"case ["go", _]: return"Sorry, can't go there!"case (["pick", obj, "up"] | ["pick", "up", obj]) ifobjin ['shovel', 'rock']:
returnf"Picking up {obj}"case ["pick", _, "up"] | ["pick", "up", _]:
return"Sorry, can't pick this up!"case _:
return"Wrong command, 2 words expected"parse_command("go south")
>>>"Going south"parse_command("go left")
>>>"Sorry, can't go there!"parse_command("pick shovel up")
>>>"Picking up shovel"parse_command("pick phone up")
>>>"Sorry, can't pick this up!"

Adapting to different structure types:

fromdataclassesimportdataclassfromdatetimeimportdatetime@dataclassclassUser:
age: intdefget_age(user: dict|User) ->int:
matchuser:
caseUser(age):
returnagecase {"dob": {"age": int(age) |float(age)}}:
returnint(age)
case {"dob": dob}:
now=datetime.now()
dob_date=datetime.strptime(dob, "%Y-%m-%d %H:%M:%S")
returnnow.year-dob_date.year
get_age({"dob": "1966-04-17 11:57:01"})
>>>56get_age({"dob": {"date": "1957-05-20T08:36:09.083Z", "age": 64}})
>>>64get_age({"dob": {"age": 39.6}})
>>>39get_age(User(age=40))
>>>40

🍴 Ternary Operator

ifa==5:
result="Five!"else:
result="Not five..."

Shorter way to write the same:

result="Five!"ifa==5else"Not five..."

🦷 Walrus Operator :=

Added in Python 3.8:

value=123print(value)
>>>123# can be written as:print(value:=123)
>>>123value>>>123

😒 Can be fine, but not the most concise way:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
# get some stats on the list: length, sum, mean valuesnum_length=len(numbers)
num_sum=sum(numbers)
stats= {
"length": num_length,
"sum": num_sum,
"mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

✅ Doing the same with less lines of code:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
stats= {
"length": (num_length:=len(numbers)), "sum": (num_sum:=sum(numbers)), "mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

🛂 == vs is

  • ==: do two objects have the same contents?
  • is: are two objects the same thing (point to the same address in memory)?
a= [1, 2, 3]
b= [1, 2, 3]
a==b>>>True
id(a) # Python id of object a>>>4435362944id(b) # Python id of object b>>>4435377344aisb# same as id(a) == id(b)>>>Falsea=baisb>>>True

ℹ️ As a consequence:

a= [1, 2, 3]
b= [1, 2, 3]
a[0] =4print(a, b)
>>> [4, 2, 3] [1, 2, 3]
a=b# make a point to the same object as b, not copying contents of b!a[0] =4# also changes b now as a and b point to the same address in memoryprint(a, b)
>>> [4, 2, 3] [4, 2, 3]

Copying an Object

🚫 Looks cryptic, and only works for lists but not for e.g. dicts:

a= [1, 2, 3]
b=a[:]
a==b>>>Trueaisb>>>False

a= [1, 2, 3]
b=a.copy()
a==b>>>Trueaisb>>>False

ℹ️ There's only one global None object

c=Noned=Nonec==d>>>Truecisd# c and d and not "copies" of None, they point to it>>>True

🧮 Named Parameters

defprint_issue_info(issue_id: str, issue_title: str)
print(f"Issue id: {issue_id}, title: {issue_title}")

😒 Can be fine:

print_issue_info("1234", "Create new thing")
>>>"Issue id: 1234, title: Create new thing"

✅ More explicit and human-readable:

print_issue_info(issue_id="1234", issue_title="Create new thing")
>>>"Issue id: 1234, title: Create new thing"print_issue_info(issue_title="Create new thing", issue_id="1234")
>>>"Issue id: 1234, title: Create new thing"

🏀 Practical Examples


📁 Reading/Writing Files

🚫 Handle errors yourself:

f=open('data.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()

✅ Use a context managerwith:

withopen("data.txt", "r") asf:
data=f.read()
withopen("data2.txt", "w") asf:
f.write(data)

🅾️ Loading/Exporting JSON

json is a built-in Python module:

importjsondata= {"a": 123, "b": None}
data_json=json.dumps(data) # dumps = dump stringprint(data_json)
>>> {"a": 123, "b": null}
data_parsed=json.loads(data_json) # loads = load stringprint(data_parsed==data)
>>>True

Reading JSON Files

$ cat file.json
{
"a": {"b": 123}
}
importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123

🌍 HTTP Requests

✅ Use the requests library:

importrequestsurl='https://api.github.com/some/endpoint'headers= {'Authentication': 'Bearer mytoken'}
r=requests.get(url, headers=headers)
print(r.status_code)
>>>200print(r.json())
>>> {"status": "OK", "message": "hi from the API"}

🔨 Tools


🔁 REPL

REPL = read-eval-print loop

Can be used to quickly try things out in a terminal:

$ python3
Python 3.10.6 (main, Aug 11 2022, 13:49:25) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license"for more information.
>>> sum(range(0, 10))
45

🐞 Debugging

breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):

value=123breakpoint()
(Pdb) value>>>123

🏁 Outro: Key Advice

  • Write as short and lean code as possible
  • Use the most recent version of Python
  • Try ideas with REPL quickly
  • Use type hinting
  • Know your basic data structures
  • Use list & dict comprehensions
  • Use f-strings
  • Use data classes
  • RealPython.com is a great source of guide on specific topics (example)

🙇‍♂️ Thank You!

About

Slides for tech talk in Camunda on Python tips and tricks (Sep 2022)

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🐍 Python: Tips & Tricks

How to Write More Pythonic Code

📽 Recording


Table of Contents


🪷 Zen of Python

importthis
>>> The Zen of Python, by Tim Peters
>>> Beautiful is better than ugly.
>>> Explicit is better than implicit.
>>> Simple is better than complex.
>>> Complex is better than complicated.
>>> Flat is better than nested.
>>> Sparse is better than dense.
>>> Readability counts.
>>> Special cases aren't special enough to break the rules.
>>> Although practicality beats purity.
>>> Errors should never pass silently.
>>> Unless explicitly silenced.
>>> In the face of ambiguity, refuse the temptation to guess.
>>> There should be one-- and preferably only one --obvious way to do it.
>>> Although that way may not be obvious at first unless you're Dutch.
>>> Now is better than never.
>>> Although never is often better than *right* now.
>>> If the implementation is hard to explain, it's a bad idea.
>>> If the implementation is easy to explain, it may be a good idea.
>>> Namespaces are one honking great idea -- let's do more of those!

📝 Python History

  • 3.6 (Dec 23, 2016): f-strings
  • 3.7 (Jun 27, 2018): data classes
  • 3.8 (Oct 14, 2019): walrus operator
  • 3.9 (Oct 5, 2020): simpler dictionary updates/merges
  • 3.10 (Oct 4, 2021): pattern matching
  • 3.11 (Oct 3, 2022): performance increases (~25%)

🎻 String Formatting

There are 4 ways to do that in Python, with a single preferred one.


The Simple (+)

🚫 Plain approach to glue strings together:

name="Bob"greeting="hello"message=greeting+" there, "+name+"!"message>>>'hello there, Bob!'

ℹ️ One can only concatenate strings with strings:

units=10items="apples"print("Currently in stock: "+str(units) +" "+items)
>>>'Currently in stock: 10 apples'

The Old (%)

🚫

name="Bob"greeting="hello"message="%s there, %s!"% (greeting, name)
message>>>'hello there, Bob!'# same result:message="%(greeting)s, %(name)s!"% {"greeting": greeting, "name": name}

The Meh (.format)

🚫

name="Bob"greeting="hello"message="{} there, {}!".format(greeting, name)
message>>>'hello there, Bob!'# can enumerate params:message="{0} there, {1}!".format(greeting, name)
message="{1} there, {0}!".format(name, greeting)
>>>'hello there, Bob!'# same result:message="{greeting} there, {name}!".format(greeting=greeting, name=name)

The Good (f-strings)

Added in Python 3.6:

✅ Looks better, more powerful, better performance (2x faster than format, 50% faster than %):

name="Bob"greeting="hello"message=f"{greeting} there, {name}!"message>>>'hello there, Bob!'

ℹ️ Any Python expressions and value formatting support:

importmathr=2print(f"Circle of radius {r} has a circumference of {2*math.pi*r}")
>>>Circleofradius2hasacircumferenceof12.566370614359172print(f"Circle of radius {r} has a circumference of {2*math.pi*r:.2f}")
>>>Circleofradius2hasacircumferenceof12.57

ℹ️ Debugging specifier = (added in Python 3.8):

x=123; y=456print(f"Calculated values: x={x}, y={y}")
>>>Calculatedvalues: x=123, y=456print(f"Calculated values: {x=}, {y=}")
>>>Calculatedvalues: x=123, y=456data= {'city': 'Berlin', 'country': 'DE'}
print(f"Result: {data=}")
>>>Result: data={'city': 'Berlin', 'country': 'DE'}

🌱 Basic Data Structures


  • Lists
  • Strings
  • Dicts
  • Tuples
  • Sets

Lists


A list is a mutable, ordered array of values

data= [1, 3, 5]
data.append(7)
data>>> [1, 3, 5, 7]
data.extend([9, 11]) # same as: data += [9, 11]>>> [1, 3, 5, 7, 9, 11]
len(data)
>>>6

Iteration

🚫 Index-based iteration loops:

foriinrange(len(data)):
print(data[i])
>>>1>>>3>>>5

✅ Every list is iterable:

forxindata:
print(x)
>>>1>>>3>>>5

ℹ️ In case one needs to access the current element's index:

fori, xinenumerate(data):
print(f"Element {i}: {x}")
>>>Element0: 1>>>Element1: 3>>>Element2: 5

Math Operations

🚫

data= [1, 2, -3, 4, 5]
sum_=0min_=data[0]
max_=data[0]
forxindata:
sum_+=xifx<min_:
min_=xifx>max_:
max_=xsum_>>>9min_>>>-3max_>>>5

data= [1, 2, -3, 4, 5]
sum(data)
>>>9min(data)
>>>-3max(data)
>>>5

List Slicing [::]

Done with so-called 🍣 sushi-operator ([::]):

array[<start_index>:<stop_index>:<step>]
  • start_index = 0 if not specified
  • stop_index = len(array) if not specified
    • it's exclusive: stop_index value is not included in the slice result
  • step_index = 1 if not specified

data= [2, 4, 6, 8, 10]
# index: 0 1 2 3 4data[1:] # same as [1::] or [1:5:1]>>> [4, 6, 8, 10]
data[1:3] # same as [1:3:1]>>> [4, 6]
data[::2] # same as [0:5:2]>>> [2, 6, 10]
data==data[0:5:1]
>>>Truedata==data[:]
>>>True

Slicing the full list with step -1 (backwards) returns a reversed version of the list:

data= [2, 4, 6, 8, 10]
data[::-1]
>>> [10, 8, 6, 4, 2]

Membership Testing With in

🚫 Implement searching algorithm yourself:

array= [1, 2, 3, 4, 5]
search_for=3found=Falseforiinrange(len(array)):
ifarray[i] ==search_for:
found=Truebreakprint(f"Found: {found}")
>>>True

✅ Let Python do it:

array= [1, 2, 3, 4, 5]
search_for=3found=search_forinarrayprint(f"Found: {found}")
>>>True

List Comprehension

Formula: [value for item in iterable] (for every item in iterable map it to value)

# range(A, B, C) = iterator of integer sequence from A to B with a step C (B is excluded) data= [x**2forxinrange(0, 5)]
data>>> [0, 1, 4, 9, 16]

List comprehension with a condition (formula: [value for item in iterable if condition])

data= [3, 2, -5, 10, 21, 7]
even= [xforxindataifx%2==0]
even>>> [2, 10]

Mapping and Filtering

Alternative to list comprehension is to use map (with a lambda function (inline function))

data=map(lambdax: x**2, range(0, 5))
print(list(data)) # `map` returns an iterator, `list` creates a materialized list of it>>> [0, 1, 4, 9, 16]

Alternative to list comprehension with a condition is to use filter (with a lambda function (inline function)):

data= [3, 2, -5, 10, 21, 7]
even=filter(lambdax: x%2==0, data)
list(even) # `filter` returns an iterator, `list` creates a materialized list of it>>> [2, 10]

Sorting Lists

data= [
{'city': 'Paris', 'country': 'FR'},
{'city': 'Berlin', 'country': 'DE'},
{'city': 'London', 'country': 'UK'}
]
# order by city name:sorted(data, key=lambdax: x['city'])
>>> [{'city': 'Berlin', 'country': 'DE'}, {'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}]
# order by country code reversed:sorted(data, key=lambdax: x['country'], reverse=True)
>>> [{'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}, {'city': 'Berlin', 'country': 'DE'}]

Truthiness

🚫 Check if the list is empty/not empty:

iflen(data) ==0:
print("List is empty")
iflen(data) >0:
print("List is not empty")

ifnotdata:
print("List is empty")
ifdata:
print("List is not empty")

Flatten List of lists

regular_list= [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
flat_list= [itemforsublistinregular_listforiteminsublist]
print('Original list', regular_list)
>>>Originallist [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
print('Transformed list', flat_list)
>>>Transformedlist [1, 2, 3, 4, 5, 6, 7, 8, 9]

Strings


A string can be seen as an iterable list of characters:

data='oslo'forletterindata:
print(letter.upper())
>>>O>>>S>>>L>>>Odata[2]
>>>'l'len(data)
>>>4data[::-1]
>>>'olso'
[ord(x) forxindata] # ord(x) == Unicode integer of character x>>> [111, 115, 108, 111]

Membership Check With in

🚫 Is substring in string:

"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False

"aura"in"restaurant">>>True"fun"in"waterfall">>>False

Stripping Whitespace Characters

data=' empty spaces, what are we living for? 'print(data.strip())
>>>'empty spaces, what are we living for?'print(data.rstrip())
>>>' empty spaces, what are we living for?'print(data.lstrip())
>>>'empty spaces, what are we living for? '

Prefix/Suffix Manipulations

Added in Python 3.9:

print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'

Tokenization

string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']

Dicts


Dict is key-val storage:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'

Iteration

Any dict is iterable:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
# iterate other keysforkeyindata:
print(f"{key}: {data[key]}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']
# iterate over keys with values:forkey, valindata.items():
print(f"{key}: {val}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']

Membership Check With in

🚫

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
found=Falseforkeyindata:
ifkey=='city':
found=Truebreakfound>>>True

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
'city'indata>>>True'population'indata>>>False'continent'notindata>>>True

Dict Comprehension

Formula: {key: val for item in iterable}

data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}

Accessing Values: [] vs. get

data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexception

data= {'a': 123, 'b': 456}
data.get('a')
>>>123data.get('b')
>>>456data.get('c')
>>>Nonedata.get('c', 'default value')
>>>'default value'

FYI, there's no set for dicts, values to be changes with the [] notation only (e.g. data["key"] = "val")


Safe Navigation

🚫 Error-prone code:

data= {'a': {'b': {'c': 123}}}
element=data['a']['b']['c']
element>>>123data= {'a': {'d': 456}}
element=data['a']['b']['c']
>>>KeyError!

✅ Robust way to inspect a dictionary:

data= {'a': {'d': 456}}
element=data.get('a', {}).get('b', {}).get('c')
element>>>None

Merging

europe= {'Madrid': 'Spain', 'Rome': 'Italy'}
asia= {'Tokyo': 'Japan', 'Manila': 'Philippines'}

Before Python 3.9:

{**europe, **asia}
>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Starting Python 3.9:

europe|asia>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Tuples


Tuple is an immutable, ordered array of values:

data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignment

Tuples can be implicit:

a=1, 2a[0]
>>>1# the same is:a= (1, 2)

Sets


Set is a unordered array of unique values:

data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptable

🧑‍🔧 Data Structures: Examples


🦄 Get Unique Elements of a List

Converting a list to a set removes duplicates:

data= [5, 2, 3, 2, 4, 3, 1]
unique=list(set(data))
unique>>> [1, 2, 3, 4, 5]

🔍 Search in a List of Objects

data= [
{"city": "Berlin", "country": "DE"},
{"city": "Sydney", "country": "AU"},
{"city": "Stockholm", "country": "SE"}
]
search=next((itemforitemindataifitem["city"] =="Sydney"), None)
search["country"]
>>>'AU'search=next((itemforitemindataifitem["city"] =="Paris"), None)
searchisNone>>>True

🪡 Is Any of the Values

🚫

ifoperation=="READ"oroperation=="WRITE":

ifoperationin ["READ", "WRITE"]:

📦 Values Unpacking


🔁 Variable Swapping

🚫 Using a temporary variable:

a=5; b=4tmp=aa=bb=tmpa>>>4b>>>5

✅ Cut to the chase:

a=5; b=4a, b=b, a# same as a, b = (b, a)a>>>4b>>>5

📭 Unpacking With Lists and Tuples

🚫

data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two

data= ['one', 'two']
a, b=dataa>>>oneb>>>two

Also works for tuples:

data= ('one', 'two', 'many', 'things')
a, b, *c=datac>>> ['many', 'things']
a, b=123, 456# same as a, b = (123, 456)a>>>123b>>>456

🌾 Data Classes


🚫 Describing objects can be done with dicts:

d1= {'name': 'Moabit', 'city': 'Berlin', 'country': 'DE', 'area': 7.72}
d2= {'name': 'Greenwich', 'city': 'London', 'country': 'UK', 'area': 47.3}
d2['city']
>>>'London'

Problem: no type hinting possible (e.g. that name is str and area should be a float) and there are no object structure restrictions in place.


🚫 We can use classes for solving it but that's bit too verbose:

classDistrict:
def__init__(self, name: str, city: str, country: str, area: float):
self.name: str=nameself.city: str=cityself.country: str=countryself.area: str=aread1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d2.city>>>'London'

✅ Using data classes (added in Python 3.7):

fromdataclassesimportdataclass@dataclassclassDistrict:
name: strcity: strcountry: strarea: float=0.0d1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d3=District(name='Brooklyn', city='New York', country='US')
d2.city>>>'London'd3.area>>>0.0

🦆 Type Hinting

There's no run-time type checking, but code with type hints allows:

  • IDEs (e.g. PyCharm) and static type checkers (e.g. mypy) to catch errors before runtime
  • to have a better, self-documented code

Basic Type Hinting

🚫

defsum_values(a, b):
returna+bsum_values(10, 3)
>>>13sum_values(10, "x")
>>>TypeError: unsupportedoperand type(s) for+: 'int'and'str'

defsum_values(a: int, b: int) ->int:
returna+bsum_values(10, "x") # IDE will highlight an error

Type Hinting & Multiple Types

Before Python 3.10:

fromtypingimportUniondefsum_values(a: Union[int, float], b: Union[int, float]) ->Union[int, float]:
# a and b can be either int or floatreturna+b

Starting Python 3.10:

defsum_values(a: int|float, b: int|float) ->int|float:
returna+b

Type Hinting & Containers

Before Python 3.10:

fromtypingimportDict, Listdefmake_list(a: str, b: str) ->List[str]:
# return type is a list of stringsreturn [a, b]
defmake_dict(k: str, v: str) ->Dict[str, str]:
# return type is a dict with string keys and string valuesreturn {k: v}
make_list("hello", "world")
>>> ['hello', 'world']
make_dict("hello", "world")
>>> {'hello': 'world'}

Starting Python 3.10:

defmake_list(a: str, b: str) ->list[str]:
return [a, b]
defmake_dict(k: str, v: str) ->dict[str, str]:
return {k: v}

Type Hinting & Generic Objects

Before Python 3.10:

fromdataclassesimportdataclassfromtypingimportList@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: List[City]) ->List[City]:
returnsorted(cities, key=lambdax: x.name)
cities= [
City(name='Madrid', country='ES'),
City(name='Berlin', country='DE'),
City(name='Edinburgh', country='UK')
]
sort_cities(cities)
>>> [City(name='Berlin', country='DE'), City(name='Edinburgh', country='UK'), City(name='Madrid', country='ES')]

Starting Python 3.10:

fromdataclassesimportdataclass@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: list[City]) ->list[City]:
returnsorted(cities, key=lambdax: x.name)

Type Hinting and Local Variables

Not only limited to inputs/outputs of a function:

Before Python 3.10:

fromtypingimportListresult: List[str] = [] # not just a list of anything!

Starting Python 3.10:

result: list[str] = []

👷‍♂️ Operators


👯‍♀️ Double Comparison

🚫

ifvalue>0andvalue<100:

if0<value<100:

🧬 Pattern Matching

Added in Python 3.10. Similar to switch statements in other languages, on steroids:

defparse_command(command: str) ->str:
matchcommand.split():
case [action, direction]:
returnf"Parsed: {action=}, {direction=}"case ["help"]: return"Help message goes here"case _:
return"Wrong command, 2 words expected"parse_command("go north")
>>>"Parsed: action='go', direction='north'"parse_command("look up")
>>>"Parsed: action='look', direction='up'"parse_command("go")
>>>"Wrong command, 2 words expected"parse_command("help")
>>>"Help message goes here"

Alias matching with as, OR matching with | and conditional matching:

defparse_command(command: str) ->str:
matchcommand.split():
case ["go", ("north"|"south") asdirection]:
returnf"Going {direction}"case ["go", _]: return"Sorry, can't go there!"case (["pick", obj, "up"] | ["pick", "up", obj]) ifobjin ['shovel', 'rock']:
returnf"Picking up {obj}"case ["pick", _, "up"] | ["pick", "up", _]:
return"Sorry, can't pick this up!"case _:
return"Wrong command, 2 words expected"parse_command("go south")
>>>"Going south"parse_command("go left")
>>>"Sorry, can't go there!"parse_command("pick shovel up")
>>>"Picking up shovel"parse_command("pick phone up")
>>>"Sorry, can't pick this up!"

Adapting to different structure types:

fromdataclassesimportdataclassfromdatetimeimportdatetime@dataclassclassUser:
age: intdefget_age(user: dict|User) ->int:
matchuser:
caseUser(age):
returnagecase {"dob": {"age": int(age) |float(age)}}:
returnint(age)
case {"dob": dob}:
now=datetime.now()
dob_date=datetime.strptime(dob, "%Y-%m-%d %H:%M:%S")
returnnow.year-dob_date.year
get_age({"dob": "1966-04-17 11:57:01"})
>>>56get_age({"dob": {"date": "1957-05-20T08:36:09.083Z", "age": 64}})
>>>64get_age({"dob": {"age": 39.6}})
>>>39get_age(User(age=40))
>>>40

🍴 Ternary Operator

ifa==5:
result="Five!"else:
result="Not five..."

Shorter way to write the same:

result="Five!"ifa==5else"Not five..."

🦷 Walrus Operator :=

Added in Python 3.8:

value=123print(value)
>>>123# can be written as:print(value:=123)
>>>123value>>>123

😒 Can be fine, but not the most concise way:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
# get some stats on the list: length, sum, mean valuesnum_length=len(numbers)
num_sum=sum(numbers)
stats= {
"length": num_length,
"sum": num_sum,
"mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

✅ Doing the same with less lines of code:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
stats= {
"length": (num_length:=len(numbers)), "sum": (num_sum:=sum(numbers)), "mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

🛂 == vs is

  • ==: do two objects have the same contents?
  • is: are two objects the same thing (point to the same address in memory)?
a= [1, 2, 3]
b= [1, 2, 3]
a==b>>>True
id(a) # Python id of object a>>>4435362944id(b) # Python id of object b>>>4435377344aisb# same as id(a) == id(b)>>>Falsea=baisb>>>True

ℹ️ As a consequence:

a= [1, 2, 3]
b= [1, 2, 3]
a[0] =4print(a, b)
>>> [4, 2, 3] [1, 2, 3]
a=b# make a point to the same object as b, not copying contents of b!a[0] =4# also changes b now as a and b point to the same address in memoryprint(a, b)
>>> [4, 2, 3] [4, 2, 3]

Copying an Object

🚫 Looks cryptic, and only works for lists but not for e.g. dicts:

a= [1, 2, 3]
b=a[:]
a==b>>>Trueaisb>>>False

a= [1, 2, 3]
b=a.copy()
a==b>>>Trueaisb>>>False

ℹ️ There's only one global None object

c=Noned=Nonec==d>>>Truecisd# c and d and not "copies" of None, they point to it>>>True

🧮 Named Parameters

defprint_issue_info(issue_id: str, issue_title: str)
print(f"Issue id: {issue_id}, title: {issue_title}")

😒 Can be fine:

print_issue_info("1234", "Create new thing")
>>>"Issue id: 1234, title: Create new thing"

✅ More explicit and human-readable:

print_issue_info(issue_id="1234", issue_title="Create new thing")
>>>"Issue id: 1234, title: Create new thing"print_issue_info(issue_title="Create new thing", issue_id="1234")
>>>"Issue id: 1234, title: Create new thing"

🏀 Practical Examples


📁 Reading/Writing Files

🚫 Handle errors yourself:

f=open('data.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()

✅ Use a context managerwith:

withopen("data.txt", "r") asf:
data=f.read()
withopen("data2.txt", "w") asf:
f.write(data)

🅾️ Loading/Exporting JSON

json is a built-in Python module:

importjsondata= {"a": 123, "b": None}
data_json=json.dumps(data) # dumps = dump stringprint(data_json)
>>> {"a": 123, "b": null}
data_parsed=json.loads(data_json) # loads = load stringprint(data_parsed==data)
>>>True

Reading JSON Files

$ cat file.json
{
"a": {"b": 123}
}
importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123

🌍 HTTP Requests

✅ Use the requests library:

importrequestsurl='https://api.github.com/some/endpoint'headers= {'Authentication': 'Bearer mytoken'}
r=requests.get(url, headers=headers)
print(r.status_code)
>>>200print(r.json())
>>> {"status": "OK", "message": "hi from the API"}

🔨 Tools


🔁 REPL

REPL = read-eval-print loop

Can be used to quickly try things out in a terminal:

$ python3
Python 3.10.6 (main, Aug 11 2022, 13:49:25) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license"for more information.
>>> sum(range(0, 10))
45

🐞 Debugging

breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):

value=123breakpoint()
(Pdb) value>>>123

🏁 Outro: Key Advice

  • Write as short and lean code as possible
  • Use the most recent version of Python
  • Try ideas with REPL quickly
  • Use type hinting
  • Know your basic data structures
  • Use list & dict comprehensions
  • Use f-strings
  • Use data classes
  • RealPython.com is a great source of guide on specific topics (example)

🙇‍♂️ Thank You!

About

Slides for tech talk in Camunda on Python tips and tricks (Sep 2022)

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🐍 Python: Tips & Tricks

How to Write More Pythonic Code

📽 Recording


Table of Contents


🪷 Zen of Python

importthis
>>> The Zen of Python, by Tim Peters
>>> Beautiful is better than ugly.
>>> Explicit is better than implicit.
>>> Simple is better than complex.
>>> Complex is better than complicated.
>>> Flat is better than nested.
>>> Sparse is better than dense.
>>> Readability counts.
>>> Special cases aren't special enough to break the rules.
>>> Although practicality beats purity.
>>> Errors should never pass silently.
>>> Unless explicitly silenced.
>>> In the face of ambiguity, refuse the temptation to guess.
>>> There should be one-- and preferably only one --obvious way to do it.
>>> Although that way may not be obvious at first unless you're Dutch.
>>> Now is better than never.
>>> Although never is often better than *right* now.
>>> If the implementation is hard to explain, it's a bad idea.
>>> If the implementation is easy to explain, it may be a good idea.
>>> Namespaces are one honking great idea -- let's do more of those!

📝 Python History

  • 3.6 (Dec 23, 2016): f-strings
  • 3.7 (Jun 27, 2018): data classes
  • 3.8 (Oct 14, 2019): walrus operator
  • 3.9 (Oct 5, 2020): simpler dictionary updates/merges
  • 3.10 (Oct 4, 2021): pattern matching
  • 3.11 (Oct 3, 2022): performance increases (~25%)

🎻 String Formatting

There are 4 ways to do that in Python, with a single preferred one.


The Simple (+)

🚫 Plain approach to glue strings together:

name="Bob"greeting="hello"message=greeting+" there, "+name+"!"message>>>'hello there, Bob!'

ℹ️ One can only concatenate strings with strings:

units=10items="apples"print("Currently in stock: "+str(units) +" "+items)
>>>'Currently in stock: 10 apples'

The Old (%)

🚫

name="Bob"greeting="hello"message="%s there, %s!"% (greeting, name)
message>>>'hello there, Bob!'# same result:message="%(greeting)s, %(name)s!"% {"greeting": greeting, "name": name}

The Meh (.format)

🚫

name="Bob"greeting="hello"message="{} there, {}!".format(greeting, name)
message>>>'hello there, Bob!'# can enumerate params:message="{0} there, {1}!".format(greeting, name)
message="{1} there, {0}!".format(name, greeting)
>>>'hello there, Bob!'# same result:message="{greeting} there, {name}!".format(greeting=greeting, name=name)

The Good (f-strings)

Added in Python 3.6:

✅ Looks better, more powerful, better performance (2x faster than format, 50% faster than %):

name="Bob"greeting="hello"message=f"{greeting} there, {name}!"message>>>'hello there, Bob!'

ℹ️ Any Python expressions and value formatting support:

importmathr=2print(f"Circle of radius {r} has a circumference of {2*math.pi*r}")
>>>Circleofradius2hasacircumferenceof12.566370614359172print(f"Circle of radius {r} has a circumference of {2*math.pi*r:.2f}")
>>>Circleofradius2hasacircumferenceof12.57

ℹ️ Debugging specifier = (added in Python 3.8):

x=123; y=456print(f"Calculated values: x={x}, y={y}")
>>>Calculatedvalues: x=123, y=456print(f"Calculated values: {x=}, {y=}")
>>>Calculatedvalues: x=123, y=456data= {'city': 'Berlin', 'country': 'DE'}
print(f"Result: {data=}")
>>>Result: data={'city': 'Berlin', 'country': 'DE'}

🌱 Basic Data Structures


  • Lists
  • Strings
  • Dicts
  • Tuples
  • Sets

Lists


A list is a mutable, ordered array of values

data= [1, 3, 5]
data.append(7)
data>>> [1, 3, 5, 7]
data.extend([9, 11]) # same as: data += [9, 11]>>> [1, 3, 5, 7, 9, 11]
len(data)
>>>6

Iteration

🚫 Index-based iteration loops:

foriinrange(len(data)):
print(data[i])
>>>1>>>3>>>5

✅ Every list is iterable:

forxindata:
print(x)
>>>1>>>3>>>5

ℹ️ In case one needs to access the current element's index:

fori, xinenumerate(data):
print(f"Element {i}: {x}")
>>>Element0: 1>>>Element1: 3>>>Element2: 5

Math Operations

🚫

data= [1, 2, -3, 4, 5]
sum_=0min_=data[0]
max_=data[0]
forxindata:
sum_+=xifx<min_:
min_=xifx>max_:
max_=xsum_>>>9min_>>>-3max_>>>5

data= [1, 2, -3, 4, 5]
sum(data)
>>>9min(data)
>>>-3max(data)
>>>5

List Slicing [::]

Done with so-called 🍣 sushi-operator ([::]):

array[<start_index>:<stop_index>:<step>]
  • start_index = 0 if not specified
  • stop_index = len(array) if not specified
    • it's exclusive: stop_index value is not included in the slice result
  • step_index = 1 if not specified

data= [2, 4, 6, 8, 10]
# index: 0 1 2 3 4data[1:] # same as [1::] or [1:5:1]>>> [4, 6, 8, 10]
data[1:3] # same as [1:3:1]>>> [4, 6]
data[::2] # same as [0:5:2]>>> [2, 6, 10]
data==data[0:5:1]
>>>Truedata==data[:]
>>>True

Slicing the full list with step -1 (backwards) returns a reversed version of the list:

data= [2, 4, 6, 8, 10]
data[::-1]
>>> [10, 8, 6, 4, 2]

Membership Testing With in

🚫 Implement searching algorithm yourself:

array= [1, 2, 3, 4, 5]
search_for=3found=Falseforiinrange(len(array)):
ifarray[i] ==search_for:
found=Truebreakprint(f"Found: {found}")
>>>True

✅ Let Python do it:

array= [1, 2, 3, 4, 5]
search_for=3found=search_forinarrayprint(f"Found: {found}")
>>>True

List Comprehension

Formula: [value for item in iterable] (for every item in iterable map it to value)

# range(A, B, C) = iterator of integer sequence from A to B with a step C (B is excluded) data= [x**2forxinrange(0, 5)]
data>>> [0, 1, 4, 9, 16]

List comprehension with a condition (formula: [value for item in iterable if condition])

data= [3, 2, -5, 10, 21, 7]
even= [xforxindataifx%2==0]
even>>> [2, 10]

Mapping and Filtering

Alternative to list comprehension is to use map (with a lambda function (inline function))

data=map(lambdax: x**2, range(0, 5))
print(list(data)) # `map` returns an iterator, `list` creates a materialized list of it>>> [0, 1, 4, 9, 16]

Alternative to list comprehension with a condition is to use filter (with a lambda function (inline function)):

data= [3, 2, -5, 10, 21, 7]
even=filter(lambdax: x%2==0, data)
list(even) # `filter` returns an iterator, `list` creates a materialized list of it>>> [2, 10]

Sorting Lists

data= [
{'city': 'Paris', 'country': 'FR'},
{'city': 'Berlin', 'country': 'DE'},
{'city': 'London', 'country': 'UK'}
]
# order by city name:sorted(data, key=lambdax: x['city'])
>>> [{'city': 'Berlin', 'country': 'DE'}, {'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}]
# order by country code reversed:sorted(data, key=lambdax: x['country'], reverse=True)
>>> [{'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}, {'city': 'Berlin', 'country': 'DE'}]

Truthiness

🚫 Check if the list is empty/not empty:

iflen(data) ==0:
print("List is empty")
iflen(data) >0:
print("List is not empty")

ifnotdata:
print("List is empty")
ifdata:
print("List is not empty")

Flatten List of lists

regular_list= [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
flat_list= [itemforsublistinregular_listforiteminsublist]
print('Original list', regular_list)
>>>Originallist [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
print('Transformed list', flat_list)
>>>Transformedlist [1, 2, 3, 4, 5, 6, 7, 8, 9]

Strings


A string can be seen as an iterable list of characters:

data='oslo'forletterindata:
print(letter.upper())
>>>O>>>S>>>L>>>Odata[2]
>>>'l'len(data)
>>>4data[::-1]
>>>'olso'
[ord(x) forxindata] # ord(x) == Unicode integer of character x>>> [111, 115, 108, 111]

Membership Check With in

🚫 Is substring in string:

"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False

"aura"in"restaurant">>>True"fun"in"waterfall">>>False

Stripping Whitespace Characters

data=' empty spaces, what are we living for? 'print(data.strip())
>>>'empty spaces, what are we living for?'print(data.rstrip())
>>>' empty spaces, what are we living for?'print(data.lstrip())
>>>'empty spaces, what are we living for? '

Prefix/Suffix Manipulations

Added in Python 3.9:

print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'

Tokenization

string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']

Dicts


Dict is key-val storage:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'

Iteration

Any dict is iterable:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
# iterate other keysforkeyindata:
print(f"{key}: {data[key]}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']
# iterate over keys with values:forkey, valindata.items():
print(f"{key}: {val}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']

Membership Check With in

🚫

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
found=Falseforkeyindata:
ifkey=='city':
found=Truebreakfound>>>True

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
'city'indata>>>True'population'indata>>>False'continent'notindata>>>True

Dict Comprehension

Formula: {key: val for item in iterable}

data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}

Accessing Values: [] vs. get

data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexception

data= {'a': 123, 'b': 456}
data.get('a')
>>>123data.get('b')
>>>456data.get('c')
>>>Nonedata.get('c', 'default value')
>>>'default value'

FYI, there's no set for dicts, values to be changes with the [] notation only (e.g. data["key"] = "val")


Safe Navigation

🚫 Error-prone code:

data= {'a': {'b': {'c': 123}}}
element=data['a']['b']['c']
element>>>123data= {'a': {'d': 456}}
element=data['a']['b']['c']
>>>KeyError!

✅ Robust way to inspect a dictionary:

data= {'a': {'d': 456}}
element=data.get('a', {}).get('b', {}).get('c')
element>>>None

Merging

europe= {'Madrid': 'Spain', 'Rome': 'Italy'}
asia= {'Tokyo': 'Japan', 'Manila': 'Philippines'}

Before Python 3.9:

{**europe, **asia}
>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Starting Python 3.9:

europe|asia>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Tuples


Tuple is an immutable, ordered array of values:

data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignment

Tuples can be implicit:

a=1, 2a[0]
>>>1# the same is:a= (1, 2)

Sets


Set is a unordered array of unique values:

data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptable

🧑‍🔧 Data Structures: Examples


🦄 Get Unique Elements of a List

Converting a list to a set removes duplicates:

data= [5, 2, 3, 2, 4, 3, 1]
unique=list(set(data))
unique>>> [1, 2, 3, 4, 5]

🔍 Search in a List of Objects

data= [
{"city": "Berlin", "country": "DE"},
{"city": "Sydney", "country": "AU"},
{"city": "Stockholm", "country": "SE"}
]
search=next((itemforitemindataifitem["city"] =="Sydney"), None)
search["country"]
>>>'AU'search=next((itemforitemindataifitem["city"] =="Paris"), None)
searchisNone>>>True

🪡 Is Any of the Values

🚫

ifoperation=="READ"oroperation=="WRITE":

ifoperationin ["READ", "WRITE"]:

📦 Values Unpacking


🔁 Variable Swapping

🚫 Using a temporary variable:

a=5; b=4tmp=aa=bb=tmpa>>>4b>>>5

✅ Cut to the chase:

a=5; b=4a, b=b, a# same as a, b = (b, a)a>>>4b>>>5

📭 Unpacking With Lists and Tuples

🚫

data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two

data= ['one', 'two']
a, b=dataa>>>oneb>>>two

Also works for tuples:

data= ('one', 'two', 'many', 'things')
a, b, *c=datac>>> ['many', 'things']
a, b=123, 456# same as a, b = (123, 456)a>>>123b>>>456

🌾 Data Classes


🚫 Describing objects can be done with dicts:

d1= {'name': 'Moabit', 'city': 'Berlin', 'country': 'DE', 'area': 7.72}
d2= {'name': 'Greenwich', 'city': 'London', 'country': 'UK', 'area': 47.3}
d2['city']
>>>'London'

Problem: no type hinting possible (e.g. that name is str and area should be a float) and there are no object structure restrictions in place.


🚫 We can use classes for solving it but that's bit too verbose:

classDistrict:
def__init__(self, name: str, city: str, country: str, area: float):
self.name: str=nameself.city: str=cityself.country: str=countryself.area: str=aread1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d2.city>>>'London'

✅ Using data classes (added in Python 3.7):

fromdataclassesimportdataclass@dataclassclassDistrict:
name: strcity: strcountry: strarea: float=0.0d1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d3=District(name='Brooklyn', city='New York', country='US')
d2.city>>>'London'd3.area>>>0.0

🦆 Type Hinting

There's no run-time type checking, but code with type hints allows:

  • IDEs (e.g. PyCharm) and static type checkers (e.g. mypy) to catch errors before runtime
  • to have a better, self-documented code

Basic Type Hinting

🚫

defsum_values(a, b):
returna+bsum_values(10, 3)
>>>13sum_values(10, "x")
>>>TypeError: unsupportedoperand type(s) for+: 'int'and'str'

defsum_values(a: int, b: int) ->int:
returna+bsum_values(10, "x") # IDE will highlight an error

Type Hinting & Multiple Types

Before Python 3.10:

fromtypingimportUniondefsum_values(a: Union[int, float], b: Union[int, float]) ->Union[int, float]:
# a and b can be either int or floatreturna+b

Starting Python 3.10:

defsum_values(a: int|float, b: int|float) ->int|float:
returna+b

Type Hinting & Containers

Before Python 3.10:

fromtypingimportDict, Listdefmake_list(a: str, b: str) ->List[str]:
# return type is a list of stringsreturn [a, b]
defmake_dict(k: str, v: str) ->Dict[str, str]:
# return type is a dict with string keys and string valuesreturn {k: v}
make_list("hello", "world")
>>> ['hello', 'world']
make_dict("hello", "world")
>>> {'hello': 'world'}

Starting Python 3.10:

defmake_list(a: str, b: str) ->list[str]:
return [a, b]
defmake_dict(k: str, v: str) ->dict[str, str]:
return {k: v}

Type Hinting & Generic Objects

Before Python 3.10:

fromdataclassesimportdataclassfromtypingimportList@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: List[City]) ->List[City]:
returnsorted(cities, key=lambdax: x.name)
cities= [
City(name='Madrid', country='ES'),
City(name='Berlin', country='DE'),
City(name='Edinburgh', country='UK')
]
sort_cities(cities)
>>> [City(name='Berlin', country='DE'), City(name='Edinburgh', country='UK'), City(name='Madrid', country='ES')]

Starting Python 3.10:

fromdataclassesimportdataclass@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: list[City]) ->list[City]:
returnsorted(cities, key=lambdax: x.name)

Type Hinting and Local Variables

Not only limited to inputs/outputs of a function:

Before Python 3.10:

fromtypingimportListresult: List[str] = [] # not just a list of anything!

Starting Python 3.10:

result: list[str] = []

👷‍♂️ Operators


👯‍♀️ Double Comparison

🚫

ifvalue>0andvalue<100:

if0<value<100:

🧬 Pattern Matching

Added in Python 3.10. Similar to switch statements in other languages, on steroids:

defparse_command(command: str) ->str:
matchcommand.split():
case [action, direction]:
returnf"Parsed: {action=}, {direction=}"case ["help"]: return"Help message goes here"case _:
return"Wrong command, 2 words expected"parse_command("go north")
>>>"Parsed: action='go', direction='north'"parse_command("look up")
>>>"Parsed: action='look', direction='up'"parse_command("go")
>>>"Wrong command, 2 words expected"parse_command("help")
>>>"Help message goes here"

Alias matching with as, OR matching with | and conditional matching:

defparse_command(command: str) ->str:
matchcommand.split():
case ["go", ("north"|"south") asdirection]:
returnf"Going {direction}"case ["go", _]: return"Sorry, can't go there!"case (["pick", obj, "up"] | ["pick", "up", obj]) ifobjin ['shovel', 'rock']:
returnf"Picking up {obj}"case ["pick", _, "up"] | ["pick", "up", _]:
return"Sorry, can't pick this up!"case _:
return"Wrong command, 2 words expected"parse_command("go south")
>>>"Going south"parse_command("go left")
>>>"Sorry, can't go there!"parse_command("pick shovel up")
>>>"Picking up shovel"parse_command("pick phone up")
>>>"Sorry, can't pick this up!"

Adapting to different structure types:

fromdataclassesimportdataclassfromdatetimeimportdatetime@dataclassclassUser:
age: intdefget_age(user: dict|User) ->int:
matchuser:
caseUser(age):
returnagecase {"dob": {"age": int(age) |float(age)}}:
returnint(age)
case {"dob": dob}:
now=datetime.now()
dob_date=datetime.strptime(dob, "%Y-%m-%d %H:%M:%S")
returnnow.year-dob_date.year
get_age({"dob": "1966-04-17 11:57:01"})
>>>56get_age({"dob": {"date": "1957-05-20T08:36:09.083Z", "age": 64}})
>>>64get_age({"dob": {"age": 39.6}})
>>>39get_age(User(age=40))
>>>40

🍴 Ternary Operator

ifa==5:
result="Five!"else:
result="Not five..."

Shorter way to write the same:

result="Five!"ifa==5else"Not five..."

🦷 Walrus Operator :=

Added in Python 3.8:

value=123print(value)
>>>123# can be written as:print(value:=123)
>>>123value>>>123

😒 Can be fine, but not the most concise way:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
# get some stats on the list: length, sum, mean valuesnum_length=len(numbers)
num_sum=sum(numbers)
stats= {
"length": num_length,
"sum": num_sum,
"mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

✅ Doing the same with less lines of code:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
stats= {
"length": (num_length:=len(numbers)), "sum": (num_sum:=sum(numbers)), "mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

🛂 == vs is

  • ==: do two objects have the same contents?
  • is: are two objects the same thing (point to the same address in memory)?
a= [1, 2, 3]
b= [1, 2, 3]
a==b>>>True
id(a) # Python id of object a>>>4435362944id(b) # Python id of object b>>>4435377344aisb# same as id(a) == id(b)>>>Falsea=baisb>>>True

ℹ️ As a consequence:

a= [1, 2, 3]
b= [1, 2, 3]
a[0] =4print(a, b)
>>> [4, 2, 3] [1, 2, 3]
a=b# make a point to the same object as b, not copying contents of b!a[0] =4# also changes b now as a and b point to the same address in memoryprint(a, b)
>>> [4, 2, 3] [4, 2, 3]

Copying an Object

🚫 Looks cryptic, and only works for lists but not for e.g. dicts:

a= [1, 2, 3]
b=a[:]
a==b>>>Trueaisb>>>False

a= [1, 2, 3]
b=a.copy()
a==b>>>Trueaisb>>>False

ℹ️ There's only one global None object

c=Noned=Nonec==d>>>Truecisd# c and d and not "copies" of None, they point to it>>>True

🧮 Named Parameters

defprint_issue_info(issue_id: str, issue_title: str)
print(f"Issue id: {issue_id}, title: {issue_title}")

😒 Can be fine:

print_issue_info("1234", "Create new thing")
>>>"Issue id: 1234, title: Create new thing"

✅ More explicit and human-readable:

print_issue_info(issue_id="1234", issue_title="Create new thing")
>>>"Issue id: 1234, title: Create new thing"print_issue_info(issue_title="Create new thing", issue_id="1234")
>>>"Issue id: 1234, title: Create new thing"

🏀 Practical Examples


📁 Reading/Writing Files

🚫 Handle errors yourself:

f=open('data.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()

✅ Use a context managerwith:

withopen("data.txt", "r") asf:
data=f.read()
withopen("data2.txt", "w") asf:
f.write(data)

🅾️ Loading/Exporting JSON

json is a built-in Python module:

importjsondata= {"a": 123, "b": None}
data_json=json.dumps(data) # dumps = dump stringprint(data_json)
>>> {"a": 123, "b": null}
data_parsed=json.loads(data_json) # loads = load stringprint(data_parsed==data)
>>>True

Reading JSON Files

$ cat file.json
{
"a": {"b": 123}
}
importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123

🌍 HTTP Requests

✅ Use the requests library:

importrequestsurl='https://api.github.com/some/endpoint'headers= {'Authentication': 'Bearer mytoken'}
r=requests.get(url, headers=headers)
print(r.status_code)
>>>200print(r.json())
>>> {"status": "OK", "message": "hi from the API"}

🔨 Tools


🔁 REPL

REPL = read-eval-print loop

Can be used to quickly try things out in a terminal:

$ python3
Python 3.10.6 (main, Aug 11 2022, 13:49:25) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license"for more information.
>>> sum(range(0, 10))
45

🐞 Debugging

breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):

value=123breakpoint()
(Pdb) value>>>123

🏁 Outro: Key Advice

  • Write as short and lean code as possible
  • Use the most recent version of Python
  • Try ideas with REPL quickly
  • Use type hinting
  • Know your basic data structures
  • Use list & dict comprehensions
  • Use f-strings
  • Use data classes
  • RealPython.com is a great source of guide on specific topics (example)

🙇‍♂️ Thank You!

About

Slides for tech talk in Camunda on Python tips and tricks (Sep 2022)

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🐍 Python: Tips & Tricks

How to Write More Pythonic Code

📽 Recording


Table of Contents


🪷 Zen of Python

importthis
>>> The Zen of Python, by Tim Peters
>>> Beautiful is better than ugly.
>>> Explicit is better than implicit.
>>> Simple is better than complex.
>>> Complex is better than complicated.
>>> Flat is better than nested.
>>> Sparse is better than dense.
>>> Readability counts.
>>> Special cases aren't special enough to break the rules.
>>> Although practicality beats purity.
>>> Errors should never pass silently.
>>> Unless explicitly silenced.
>>> In the face of ambiguity, refuse the temptation to guess.
>>> There should be one-- and preferably only one --obvious way to do it.
>>> Although that way may not be obvious at first unless you're Dutch.
>>> Now is better than never.
>>> Although never is often better than *right* now.
>>> If the implementation is hard to explain, it's a bad idea.
>>> If the implementation is easy to explain, it may be a good idea.
>>> Namespaces are one honking great idea -- let's do more of those!

📝 Python History

  • 3.6 (Dec 23, 2016): f-strings
  • 3.7 (Jun 27, 2018): data classes
  • 3.8 (Oct 14, 2019): walrus operator
  • 3.9 (Oct 5, 2020): simpler dictionary updates/merges
  • 3.10 (Oct 4, 2021): pattern matching
  • 3.11 (Oct 3, 2022): performance increases (~25%)

🎻 String Formatting

There are 4 ways to do that in Python, with a single preferred one.


The Simple (+)

🚫 Plain approach to glue strings together:

name="Bob"greeting="hello"message=greeting+" there, "+name+"!"message>>>'hello there, Bob!'

ℹ️ One can only concatenate strings with strings:

units=10items="apples"print("Currently in stock: "+str(units) +" "+items)
>>>'Currently in stock: 10 apples'

The Old (%)

🚫

name="Bob"greeting="hello"message="%s there, %s!"% (greeting, name)
message>>>'hello there, Bob!'# same result:message="%(greeting)s, %(name)s!"% {"greeting": greeting, "name": name}

The Meh (.format)

🚫

name="Bob"greeting="hello"message="{} there, {}!".format(greeting, name)
message>>>'hello there, Bob!'# can enumerate params:message="{0} there, {1}!".format(greeting, name)
message="{1} there, {0}!".format(name, greeting)
>>>'hello there, Bob!'# same result:message="{greeting} there, {name}!".format(greeting=greeting, name=name)

The Good (f-strings)

Added in Python 3.6:

✅ Looks better, more powerful, better performance (2x faster than format, 50% faster than %):

name="Bob"greeting="hello"message=f"{greeting} there, {name}!"message>>>'hello there, Bob!'

ℹ️ Any Python expressions and value formatting support:

importmathr=2print(f"Circle of radius {r} has a circumference of {2*math.pi*r}")
>>>Circleofradius2hasacircumferenceof12.566370614359172print(f"Circle of radius {r} has a circumference of {2*math.pi*r:.2f}")
>>>Circleofradius2hasacircumferenceof12.57

ℹ️ Debugging specifier = (added in Python 3.8):

x=123; y=456print(f"Calculated values: x={x}, y={y}")
>>>Calculatedvalues: x=123, y=456print(f"Calculated values: {x=}, {y=}")
>>>Calculatedvalues: x=123, y=456data= {'city': 'Berlin', 'country': 'DE'}
print(f"Result: {data=}")
>>>Result: data={'city': 'Berlin', 'country': 'DE'}

🌱 Basic Data Structures


  • Lists
  • Strings
  • Dicts
  • Tuples
  • Sets

Lists


A list is a mutable, ordered array of values

data= [1, 3, 5]
data.append(7)
data>>> [1, 3, 5, 7]
data.extend([9, 11]) # same as: data += [9, 11]>>> [1, 3, 5, 7, 9, 11]
len(data)
>>>6

Iteration

🚫 Index-based iteration loops:

foriinrange(len(data)):
print(data[i])
>>>1>>>3>>>5

✅ Every list is iterable:

forxindata:
print(x)
>>>1>>>3>>>5

ℹ️ In case one needs to access the current element's index:

fori, xinenumerate(data):
print(f"Element {i}: {x}")
>>>Element0: 1>>>Element1: 3>>>Element2: 5

Math Operations

🚫

data= [1, 2, -3, 4, 5]
sum_=0min_=data[0]
max_=data[0]
forxindata:
sum_+=xifx<min_:
min_=xifx>max_:
max_=xsum_>>>9min_>>>-3max_>>>5

data= [1, 2, -3, 4, 5]
sum(data)
>>>9min(data)
>>>-3max(data)
>>>5

List Slicing [::]

Done with so-called 🍣 sushi-operator ([::]):

array[<start_index>:<stop_index>:<step>]
  • start_index = 0 if not specified
  • stop_index = len(array) if not specified
    • it's exclusive: stop_index value is not included in the slice result
  • step_index = 1 if not specified

data= [2, 4, 6, 8, 10]
# index: 0 1 2 3 4data[1:] # same as [1::] or [1:5:1]>>> [4, 6, 8, 10]
data[1:3] # same as [1:3:1]>>> [4, 6]
data[::2] # same as [0:5:2]>>> [2, 6, 10]
data==data[0:5:1]
>>>Truedata==data[:]
>>>True

Slicing the full list with step -1 (backwards) returns a reversed version of the list:

data= [2, 4, 6, 8, 10]
data[::-1]
>>> [10, 8, 6, 4, 2]

Membership Testing With in

🚫 Implement searching algorithm yourself:

array= [1, 2, 3, 4, 5]
search_for=3found=Falseforiinrange(len(array)):
ifarray[i] ==search_for:
found=Truebreakprint(f"Found: {found}")
>>>True

✅ Let Python do it:

array= [1, 2, 3, 4, 5]
search_for=3found=search_forinarrayprint(f"Found: {found}")
>>>True

List Comprehension

Formula: [value for item in iterable] (for every item in iterable map it to value)

# range(A, B, C) = iterator of integer sequence from A to B with a step C (B is excluded) data= [x**2forxinrange(0, 5)]
data>>> [0, 1, 4, 9, 16]

List comprehension with a condition (formula: [value for item in iterable if condition])

data= [3, 2, -5, 10, 21, 7]
even= [xforxindataifx%2==0]
even>>> [2, 10]

Mapping and Filtering

Alternative to list comprehension is to use map (with a lambda function (inline function))

data=map(lambdax: x**2, range(0, 5))
print(list(data)) # `map` returns an iterator, `list` creates a materialized list of it>>> [0, 1, 4, 9, 16]

Alternative to list comprehension with a condition is to use filter (with a lambda function (inline function)):

data= [3, 2, -5, 10, 21, 7]
even=filter(lambdax: x%2==0, data)
list(even) # `filter` returns an iterator, `list` creates a materialized list of it>>> [2, 10]

Sorting Lists

data= [
{'city': 'Paris', 'country': 'FR'},
{'city': 'Berlin', 'country': 'DE'},
{'city': 'London', 'country': 'UK'}
]
# order by city name:sorted(data, key=lambdax: x['city'])
>>> [{'city': 'Berlin', 'country': 'DE'}, {'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}]
# order by country code reversed:sorted(data, key=lambdax: x['country'], reverse=True)
>>> [{'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}, {'city': 'Berlin', 'country': 'DE'}]

Truthiness

🚫 Check if the list is empty/not empty:

iflen(data) ==0:
print("List is empty")
iflen(data) >0:
print("List is not empty")

ifnotdata:
print("List is empty")
ifdata:
print("List is not empty")

Flatten List of lists

regular_list= [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
flat_list= [itemforsublistinregular_listforiteminsublist]
print('Original list', regular_list)
>>>Originallist [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
print('Transformed list', flat_list)
>>>Transformedlist [1, 2, 3, 4, 5, 6, 7, 8, 9]

Strings


A string can be seen as an iterable list of characters:

data='oslo'forletterindata:
print(letter.upper())
>>>O>>>S>>>L>>>Odata[2]
>>>'l'len(data)
>>>4data[::-1]
>>>'olso'
[ord(x) forxindata] # ord(x) == Unicode integer of character x>>> [111, 115, 108, 111]

Membership Check With in

🚫 Is substring in string:

"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False

"aura"in"restaurant">>>True"fun"in"waterfall">>>False

Stripping Whitespace Characters

data=' empty spaces, what are we living for? 'print(data.strip())
>>>'empty spaces, what are we living for?'print(data.rstrip())
>>>' empty spaces, what are we living for?'print(data.lstrip())
>>>'empty spaces, what are we living for? '

Prefix/Suffix Manipulations

Added in Python 3.9:

print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'

Tokenization

string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']

Dicts


Dict is key-val storage:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'

Iteration

Any dict is iterable:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
# iterate other keysforkeyindata:
print(f"{key}: {data[key]}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']
# iterate over keys with values:forkey, valindata.items():
print(f"{key}: {val}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']

Membership Check With in

🚫

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
found=Falseforkeyindata:
ifkey=='city':
found=Truebreakfound>>>True

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
'city'indata>>>True'population'indata>>>False'continent'notindata>>>True

Dict Comprehension

Formula: {key: val for item in iterable}

data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}

Accessing Values: [] vs. get

data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexception

data= {'a': 123, 'b': 456}
data.get('a')
>>>123data.get('b')
>>>456data.get('c')
>>>Nonedata.get('c', 'default value')
>>>'default value'

FYI, there's no set for dicts, values to be changes with the [] notation only (e.g. data["key"] = "val")


Safe Navigation

🚫 Error-prone code:

data= {'a': {'b': {'c': 123}}}
element=data['a']['b']['c']
element>>>123data= {'a': {'d': 456}}
element=data['a']['b']['c']
>>>KeyError!

✅ Robust way to inspect a dictionary:

data= {'a': {'d': 456}}
element=data.get('a', {}).get('b', {}).get('c')
element>>>None

Merging

europe= {'Madrid': 'Spain', 'Rome': 'Italy'}
asia= {'Tokyo': 'Japan', 'Manila': 'Philippines'}

Before Python 3.9:

{**europe, **asia}
>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Starting Python 3.9:

europe|asia>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Tuples


Tuple is an immutable, ordered array of values:

data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignment

Tuples can be implicit:

a=1, 2a[0]
>>>1# the same is:a= (1, 2)

Sets


Set is a unordered array of unique values:

data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptable

🧑‍🔧 Data Structures: Examples


🦄 Get Unique Elements of a List

Converting a list to a set removes duplicates:

data= [5, 2, 3, 2, 4, 3, 1]
unique=list(set(data))
unique>>> [1, 2, 3, 4, 5]

🔍 Search in a List of Objects

data= [
{"city": "Berlin", "country": "DE"},
{"city": "Sydney", "country": "AU"},
{"city": "Stockholm", "country": "SE"}
]
search=next((itemforitemindataifitem["city"] =="Sydney"), None)
search["country"]
>>>'AU'search=next((itemforitemindataifitem["city"] =="Paris"), None)
searchisNone>>>True

🪡 Is Any of the Values

🚫

ifoperation=="READ"oroperation=="WRITE":

ifoperationin ["READ", "WRITE"]:

📦 Values Unpacking


🔁 Variable Swapping

🚫 Using a temporary variable:

a=5; b=4tmp=aa=bb=tmpa>>>4b>>>5

✅ Cut to the chase:

a=5; b=4a, b=b, a# same as a, b = (b, a)a>>>4b>>>5

📭 Unpacking With Lists and Tuples

🚫

data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two

data= ['one', 'two']
a, b=dataa>>>oneb>>>two

Also works for tuples:

data= ('one', 'two', 'many', 'things')
a, b, *c=datac>>> ['many', 'things']
a, b=123, 456# same as a, b = (123, 456)a>>>123b>>>456

🌾 Data Classes


🚫 Describing objects can be done with dicts:

d1= {'name': 'Moabit', 'city': 'Berlin', 'country': 'DE', 'area': 7.72}
d2= {'name': 'Greenwich', 'city': 'London', 'country': 'UK', 'area': 47.3}
d2['city']
>>>'London'

Problem: no type hinting possible (e.g. that name is str and area should be a float) and there are no object structure restrictions in place.


🚫 We can use classes for solving it but that's bit too verbose:

classDistrict:
def__init__(self, name: str, city: str, country: str, area: float):
self.name: str=nameself.city: str=cityself.country: str=countryself.area: str=aread1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d2.city>>>'London'

✅ Using data classes (added in Python 3.7):

fromdataclassesimportdataclass@dataclassclassDistrict:
name: strcity: strcountry: strarea: float=0.0d1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d3=District(name='Brooklyn', city='New York', country='US')
d2.city>>>'London'd3.area>>>0.0

🦆 Type Hinting

There's no run-time type checking, but code with type hints allows:

  • IDEs (e.g. PyCharm) and static type checkers (e.g. mypy) to catch errors before runtime
  • to have a better, self-documented code

Basic Type Hinting

🚫

defsum_values(a, b):
returna+bsum_values(10, 3)
>>>13sum_values(10, "x")
>>>TypeError: unsupportedoperand type(s) for+: 'int'and'str'

defsum_values(a: int, b: int) ->int:
returna+bsum_values(10, "x") # IDE will highlight an error

Type Hinting & Multiple Types

Before Python 3.10:

fromtypingimportUniondefsum_values(a: Union[int, float], b: Union[int, float]) ->Union[int, float]:
# a and b can be either int or floatreturna+b

Starting Python 3.10:

defsum_values(a: int|float, b: int|float) ->int|float:
returna+b

Type Hinting & Containers

Before Python 3.10:

fromtypingimportDict, Listdefmake_list(a: str, b: str) ->List[str]:
# return type is a list of stringsreturn [a, b]
defmake_dict(k: str, v: str) ->Dict[str, str]:
# return type is a dict with string keys and string valuesreturn {k: v}
make_list("hello", "world")
>>> ['hello', 'world']
make_dict("hello", "world")
>>> {'hello': 'world'}

Starting Python 3.10:

defmake_list(a: str, b: str) ->list[str]:
return [a, b]
defmake_dict(k: str, v: str) ->dict[str, str]:
return {k: v}

Type Hinting & Generic Objects

Before Python 3.10:

fromdataclassesimportdataclassfromtypingimportList@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: List[City]) ->List[City]:
returnsorted(cities, key=lambdax: x.name)
cities= [
City(name='Madrid', country='ES'),
City(name='Berlin', country='DE'),
City(name='Edinburgh', country='UK')
]
sort_cities(cities)
>>> [City(name='Berlin', country='DE'), City(name='Edinburgh', country='UK'), City(name='Madrid', country='ES')]

Starting Python 3.10:

fromdataclassesimportdataclass@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: list[City]) ->list[City]:
returnsorted(cities, key=lambdax: x.name)

Type Hinting and Local Variables

Not only limited to inputs/outputs of a function:

Before Python 3.10:

fromtypingimportListresult: List[str] = [] # not just a list of anything!

Starting Python 3.10:

result: list[str] = []

👷‍♂️ Operators


👯‍♀️ Double Comparison

🚫

ifvalue>0andvalue<100:

if0<value<100:

🧬 Pattern Matching

Added in Python 3.10. Similar to switch statements in other languages, on steroids:

defparse_command(command: str) ->str:
matchcommand.split():
case [action, direction]:
returnf"Parsed: {action=}, {direction=}"case ["help"]: return"Help message goes here"case _:
return"Wrong command, 2 words expected"parse_command("go north")
>>>"Parsed: action='go', direction='north'"parse_command("look up")
>>>"Parsed: action='look', direction='up'"parse_command("go")
>>>"Wrong command, 2 words expected"parse_command("help")
>>>"Help message goes here"

Alias matching with as, OR matching with | and conditional matching:

defparse_command(command: str) ->str:
matchcommand.split():
case ["go", ("north"|"south") asdirection]:
returnf"Going {direction}"case ["go", _]: return"Sorry, can't go there!"case (["pick", obj, "up"] | ["pick", "up", obj]) ifobjin ['shovel', 'rock']:
returnf"Picking up {obj}"case ["pick", _, "up"] | ["pick", "up", _]:
return"Sorry, can't pick this up!"case _:
return"Wrong command, 2 words expected"parse_command("go south")
>>>"Going south"parse_command("go left")
>>>"Sorry, can't go there!"parse_command("pick shovel up")
>>>"Picking up shovel"parse_command("pick phone up")
>>>"Sorry, can't pick this up!"

Adapting to different structure types:

fromdataclassesimportdataclassfromdatetimeimportdatetime@dataclassclassUser:
age: intdefget_age(user: dict|User) ->int:
matchuser:
caseUser(age):
returnagecase {"dob": {"age": int(age) |float(age)}}:
returnint(age)
case {"dob": dob}:
now=datetime.now()
dob_date=datetime.strptime(dob, "%Y-%m-%d %H:%M:%S")
returnnow.year-dob_date.year
get_age({"dob": "1966-04-17 11:57:01"})
>>>56get_age({"dob": {"date": "1957-05-20T08:36:09.083Z", "age": 64}})
>>>64get_age({"dob": {"age": 39.6}})
>>>39get_age(User(age=40))
>>>40

🍴 Ternary Operator

ifa==5:
result="Five!"else:
result="Not five..."

Shorter way to write the same:

result="Five!"ifa==5else"Not five..."

🦷 Walrus Operator :=

Added in Python 3.8:

value=123print(value)
>>>123# can be written as:print(value:=123)
>>>123value>>>123

😒 Can be fine, but not the most concise way:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
# get some stats on the list: length, sum, mean valuesnum_length=len(numbers)
num_sum=sum(numbers)
stats= {
"length": num_length,
"sum": num_sum,
"mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

✅ Doing the same with less lines of code:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
stats= {
"length": (num_length:=len(numbers)), "sum": (num_sum:=sum(numbers)), "mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

🛂 == vs is

  • ==: do two objects have the same contents?
  • is: are two objects the same thing (point to the same address in memory)?
a= [1, 2, 3]
b= [1, 2, 3]
a==b>>>True
id(a) # Python id of object a>>>4435362944id(b) # Python id of object b>>>4435377344aisb# same as id(a) == id(b)>>>Falsea=baisb>>>True

ℹ️ As a consequence:

a= [1, 2, 3]
b= [1, 2, 3]
a[0] =4print(a, b)
>>> [4, 2, 3] [1, 2, 3]
a=b# make a point to the same object as b, not copying contents of b!a[0] =4# also changes b now as a and b point to the same address in memoryprint(a, b)
>>> [4, 2, 3] [4, 2, 3]

Copying an Object

🚫 Looks cryptic, and only works for lists but not for e.g. dicts:

a= [1, 2, 3]
b=a[:]
a==b>>>Trueaisb>>>False

a= [1, 2, 3]
b=a.copy()
a==b>>>Trueaisb>>>False

ℹ️ There's only one global None object

c=Noned=Nonec==d>>>Truecisd# c and d and not "copies" of None, they point to it>>>True

🧮 Named Parameters

defprint_issue_info(issue_id: str, issue_title: str)
print(f"Issue id: {issue_id}, title: {issue_title}")

😒 Can be fine:

print_issue_info("1234", "Create new thing")
>>>"Issue id: 1234, title: Create new thing"

✅ More explicit and human-readable:

print_issue_info(issue_id="1234", issue_title="Create new thing")
>>>"Issue id: 1234, title: Create new thing"print_issue_info(issue_title="Create new thing", issue_id="1234")
>>>"Issue id: 1234, title: Create new thing"

🏀 Practical Examples


📁 Reading/Writing Files

🚫 Handle errors yourself:

f=open('data.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()

✅ Use a context managerwith:

withopen("data.txt", "r") asf:
data=f.read()
withopen("data2.txt", "w") asf:
f.write(data)

🅾️ Loading/Exporting JSON

json is a built-in Python module:

importjsondata= {"a": 123, "b": None}
data_json=json.dumps(data) # dumps = dump stringprint(data_json)
>>> {"a": 123, "b": null}
data_parsed=json.loads(data_json) # loads = load stringprint(data_parsed==data)
>>>True

Reading JSON Files

$ cat file.json
{
"a": {"b": 123}
}
importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123

🌍 HTTP Requests

✅ Use the requests library:

importrequestsurl='https://api.github.com/some/endpoint'headers= {'Authentication': 'Bearer mytoken'}
r=requests.get(url, headers=headers)
print(r.status_code)
>>>200print(r.json())
>>> {"status": "OK", "message": "hi from the API"}

🔨 Tools


🔁 REPL

REPL = read-eval-print loop

Can be used to quickly try things out in a terminal:

$ python3
Python 3.10.6 (main, Aug 11 2022, 13:49:25) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license"for more information.
>>> sum(range(0, 10))
45

🐞 Debugging

breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):

value=123breakpoint()
(Pdb) value>>>123

🏁 Outro: Key Advice

  • Write as short and lean code as possible
  • Use the most recent version of Python
  • Try ideas with REPL quickly
  • Use type hinting
  • Know your basic data structures
  • Use list & dict comprehensions
  • Use f-strings
  • Use data classes
  • RealPython.com is a great source of guide on specific topics (example)

🙇‍♂️ Thank You!

About

Slides for tech talk in Camunda on Python tips and tricks (Sep 2022)

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🐍 Python: Tips & Tricks

How to Write More Pythonic Code

📽 Recording


Table of Contents


🪷 Zen of Python

importthis
>>> The Zen of Python, by Tim Peters
>>> Beautiful is better than ugly.
>>> Explicit is better than implicit.
>>> Simple is better than complex.
>>> Complex is better than complicated.
>>> Flat is better than nested.
>>> Sparse is better than dense.
>>> Readability counts.
>>> Special cases aren't special enough to break the rules.
>>> Although practicality beats purity.
>>> Errors should never pass silently.
>>> Unless explicitly silenced.
>>> In the face of ambiguity, refuse the temptation to guess.
>>> There should be one-- and preferably only one --obvious way to do it.
>>> Although that way may not be obvious at first unless you're Dutch.
>>> Now is better than never.
>>> Although never is often better than *right* now.
>>> If the implementation is hard to explain, it's a bad idea.
>>> If the implementation is easy to explain, it may be a good idea.
>>> Namespaces are one honking great idea -- let's do more of those!

📝 Python History

  • 3.6 (Dec 23, 2016): f-strings
  • 3.7 (Jun 27, 2018): data classes
  • 3.8 (Oct 14, 2019): walrus operator
  • 3.9 (Oct 5, 2020): simpler dictionary updates/merges
  • 3.10 (Oct 4, 2021): pattern matching
  • 3.11 (Oct 3, 2022): performance increases (~25%)

🎻 String Formatting

There are 4 ways to do that in Python, with a single preferred one.


The Simple (+)

🚫 Plain approach to glue strings together:

name="Bob"greeting="hello"message=greeting+" there, "+name+"!"message>>>'hello there, Bob!'

ℹ️ One can only concatenate strings with strings:

units=10items="apples"print("Currently in stock: "+str(units) +" "+items)
>>>'Currently in stock: 10 apples'

The Old (%)

🚫

name="Bob"greeting="hello"message="%s there, %s!"% (greeting, name)
message>>>'hello there, Bob!'# same result:message="%(greeting)s, %(name)s!"% {"greeting": greeting, "name": name}

The Meh (.format)

🚫

name="Bob"greeting="hello"message="{} there, {}!".format(greeting, name)
message>>>'hello there, Bob!'# can enumerate params:message="{0} there, {1}!".format(greeting, name)
message="{1} there, {0}!".format(name, greeting)
>>>'hello there, Bob!'# same result:message="{greeting} there, {name}!".format(greeting=greeting, name=name)

The Good (f-strings)

Added in Python 3.6:

✅ Looks better, more powerful, better performance (2x faster than format, 50% faster than %):

name="Bob"greeting="hello"message=f"{greeting} there, {name}!"message>>>'hello there, Bob!'

ℹ️ Any Python expressions and value formatting support:

importmathr=2print(f"Circle of radius {r} has a circumference of {2*math.pi*r}")
>>>Circleofradius2hasacircumferenceof12.566370614359172print(f"Circle of radius {r} has a circumference of {2*math.pi*r:.2f}")
>>>Circleofradius2hasacircumferenceof12.57

ℹ️ Debugging specifier = (added in Python 3.8):

x=123; y=456print(f"Calculated values: x={x}, y={y}")
>>>Calculatedvalues: x=123, y=456print(f"Calculated values: {x=}, {y=}")
>>>Calculatedvalues: x=123, y=456data= {'city': 'Berlin', 'country': 'DE'}
print(f"Result: {data=}")
>>>Result: data={'city': 'Berlin', 'country': 'DE'}

🌱 Basic Data Structures


  • Lists
  • Strings
  • Dicts
  • Tuples
  • Sets

Lists


A list is a mutable, ordered array of values

data= [1, 3, 5]
data.append(7)
data>>> [1, 3, 5, 7]
data.extend([9, 11]) # same as: data += [9, 11]>>> [1, 3, 5, 7, 9, 11]
len(data)
>>>6

Iteration

🚫 Index-based iteration loops:

foriinrange(len(data)):
print(data[i])
>>>1>>>3>>>5

✅ Every list is iterable:

forxindata:
print(x)
>>>1>>>3>>>5

ℹ️ In case one needs to access the current element's index:

fori, xinenumerate(data):
print(f"Element {i}: {x}")
>>>Element0: 1>>>Element1: 3>>>Element2: 5

Math Operations

🚫

data= [1, 2, -3, 4, 5]
sum_=0min_=data[0]
max_=data[0]
forxindata:
sum_+=xifx<min_:
min_=xifx>max_:
max_=xsum_>>>9min_>>>-3max_>>>5

data= [1, 2, -3, 4, 5]
sum(data)
>>>9min(data)
>>>-3max(data)
>>>5

List Slicing [::]

Done with so-called 🍣 sushi-operator ([::]):

array[<start_index>:<stop_index>:<step>]
  • start_index = 0 if not specified
  • stop_index = len(array) if not specified
    • it's exclusive: stop_index value is not included in the slice result
  • step_index = 1 if not specified

data= [2, 4, 6, 8, 10]
# index: 0 1 2 3 4data[1:] # same as [1::] or [1:5:1]>>> [4, 6, 8, 10]
data[1:3] # same as [1:3:1]>>> [4, 6]
data[::2] # same as [0:5:2]>>> [2, 6, 10]
data==data[0:5:1]
>>>Truedata==data[:]
>>>True

Slicing the full list with step -1 (backwards) returns a reversed version of the list:

data= [2, 4, 6, 8, 10]
data[::-1]
>>> [10, 8, 6, 4, 2]

Membership Testing With in

🚫 Implement searching algorithm yourself:

array= [1, 2, 3, 4, 5]
search_for=3found=Falseforiinrange(len(array)):
ifarray[i] ==search_for:
found=Truebreakprint(f"Found: {found}")
>>>True

✅ Let Python do it:

array= [1, 2, 3, 4, 5]
search_for=3found=search_forinarrayprint(f"Found: {found}")
>>>True

List Comprehension

Formula: [value for item in iterable] (for every item in iterable map it to value)

# range(A, B, C) = iterator of integer sequence from A to B with a step C (B is excluded) data= [x**2forxinrange(0, 5)]
data>>> [0, 1, 4, 9, 16]

List comprehension with a condition (formula: [value for item in iterable if condition])

data= [3, 2, -5, 10, 21, 7]
even= [xforxindataifx%2==0]
even>>> [2, 10]

Mapping and Filtering

Alternative to list comprehension is to use map (with a lambda function (inline function))

data=map(lambdax: x**2, range(0, 5))
print(list(data)) # `map` returns an iterator, `list` creates a materialized list of it>>> [0, 1, 4, 9, 16]

Alternative to list comprehension with a condition is to use filter (with a lambda function (inline function)):

data= [3, 2, -5, 10, 21, 7]
even=filter(lambdax: x%2==0, data)
list(even) # `filter` returns an iterator, `list` creates a materialized list of it>>> [2, 10]

Sorting Lists

data= [
{'city': 'Paris', 'country': 'FR'},
{'city': 'Berlin', 'country': 'DE'},
{'city': 'London', 'country': 'UK'}
]
# order by city name:sorted(data, key=lambdax: x['city'])
>>> [{'city': 'Berlin', 'country': 'DE'}, {'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}]
# order by country code reversed:sorted(data, key=lambdax: x['country'], reverse=True)
>>> [{'city': 'London', 'country': 'UK'}, {'city': 'Paris', 'country': 'FR'}, {'city': 'Berlin', 'country': 'DE'}]

Truthiness

🚫 Check if the list is empty/not empty:

iflen(data) ==0:
print("List is empty")
iflen(data) >0:
print("List is not empty")

ifnotdata:
print("List is empty")
ifdata:
print("List is not empty")

Flatten List of lists

regular_list= [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
flat_list= [itemforsublistinregular_listforiteminsublist]
print('Original list', regular_list)
>>>Originallist [[1, 2, 3, 4], [5, 6, 7], [8, 9]]
print('Transformed list', flat_list)
>>>Transformedlist [1, 2, 3, 4, 5, 6, 7, 8, 9]

Strings


A string can be seen as an iterable list of characters:

data='oslo'forletterindata:
print(letter.upper())
>>>O>>>S>>>L>>>Odata[2]
>>>'l'len(data)
>>>4data[::-1]
>>>'olso'
[ord(x) forxindata] # ord(x) == Unicode integer of character x>>> [111, 115, 108, 111]

Membership Check With in

🚫 Is substring in string:

"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False

"aura"in"restaurant">>>True"fun"in"waterfall">>>False

Stripping Whitespace Characters

data=' empty spaces, what are we living for? 'print(data.strip())
>>>'empty spaces, what are we living for?'print(data.rstrip())
>>>' empty spaces, what are we living for?'print(data.lstrip())
>>>'empty spaces, what are we living for? '

Prefix/Suffix Manipulations

Added in Python 3.9:

print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'

Tokenization

string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']

Dicts


Dict is key-val storage:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'

Iteration

Any dict is iterable:

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
# iterate other keysforkeyindata:
print(f"{key}: {data[key]}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']
# iterate over keys with values:forkey, valindata.items():
print(f"{key}: {val}")
>>>city: Berlin>>>country: DE>>>areas: ['Moabit', 'Mitte', 'Westend']

Membership Check With in

🚫

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
found=Falseforkeyindata:
ifkey=='city':
found=Truebreakfound>>>True

data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
'city'indata>>>True'population'indata>>>False'continent'notindata>>>True

Dict Comprehension

Formula: {key: val for item in iterable}

data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}

Accessing Values: [] vs. get

data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexception

data= {'a': 123, 'b': 456}
data.get('a')
>>>123data.get('b')
>>>456data.get('c')
>>>Nonedata.get('c', 'default value')
>>>'default value'

FYI, there's no set for dicts, values to be changes with the [] notation only (e.g. data["key"] = "val")


Safe Navigation

🚫 Error-prone code:

data= {'a': {'b': {'c': 123}}}
element=data['a']['b']['c']
element>>>123data= {'a': {'d': 456}}
element=data['a']['b']['c']
>>>KeyError!

✅ Robust way to inspect a dictionary:

data= {'a': {'d': 456}}
element=data.get('a', {}).get('b', {}).get('c')
element>>>None

Merging

europe= {'Madrid': 'Spain', 'Rome': 'Italy'}
asia= {'Tokyo': 'Japan', 'Manila': 'Philippines'}

Before Python 3.9:

{**europe, **asia}
>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Starting Python 3.9:

europe|asia>>> {'Madrid': 'Spain', 'Rome': 'Italy', 'Tokyo': 'Japan', 'Manila': 'Philippines'}

Tuples


Tuple is an immutable, ordered array of values:

data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignment

Tuples can be implicit:

a=1, 2a[0]
>>>1# the same is:a= (1, 2)

Sets


Set is a unordered array of unique values:

data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptable

🧑‍🔧 Data Structures: Examples


🦄 Get Unique Elements of a List

Converting a list to a set removes duplicates:

data= [5, 2, 3, 2, 4, 3, 1]
unique=list(set(data))
unique>>> [1, 2, 3, 4, 5]

🔍 Search in a List of Objects

data= [
{"city": "Berlin", "country": "DE"},
{"city": "Sydney", "country": "AU"},
{"city": "Stockholm", "country": "SE"}
]
search=next((itemforitemindataifitem["city"] =="Sydney"), None)
search["country"]
>>>'AU'search=next((itemforitemindataifitem["city"] =="Paris"), None)
searchisNone>>>True

🪡 Is Any of the Values

🚫

ifoperation=="READ"oroperation=="WRITE":

ifoperationin ["READ", "WRITE"]:

📦 Values Unpacking


🔁 Variable Swapping

🚫 Using a temporary variable:

a=5; b=4tmp=aa=bb=tmpa>>>4b>>>5

✅ Cut to the chase:

a=5; b=4a, b=b, a# same as a, b = (b, a)a>>>4b>>>5

📭 Unpacking With Lists and Tuples

🚫

data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two

data= ['one', 'two']
a, b=dataa>>>oneb>>>two

Also works for tuples:

data= ('one', 'two', 'many', 'things')
a, b, *c=datac>>> ['many', 'things']
a, b=123, 456# same as a, b = (123, 456)a>>>123b>>>456

🌾 Data Classes


🚫 Describing objects can be done with dicts:

d1= {'name': 'Moabit', 'city': 'Berlin', 'country': 'DE', 'area': 7.72}
d2= {'name': 'Greenwich', 'city': 'London', 'country': 'UK', 'area': 47.3}
d2['city']
>>>'London'

Problem: no type hinting possible (e.g. that name is str and area should be a float) and there are no object structure restrictions in place.


🚫 We can use classes for solving it but that's bit too verbose:

classDistrict:
def__init__(self, name: str, city: str, country: str, area: float):
self.name: str=nameself.city: str=cityself.country: str=countryself.area: str=aread1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d2.city>>>'London'

✅ Using data classes (added in Python 3.7):

fromdataclassesimportdataclass@dataclassclassDistrict:
name: strcity: strcountry: strarea: float=0.0d1=District(name='Moabit', city='Berlin', country='DE', area=7.72)
d2=District(name='Greenwich', city='London', country='UK', area=47.3)
d3=District(name='Brooklyn', city='New York', country='US')
d2.city>>>'London'd3.area>>>0.0

🦆 Type Hinting

There's no run-time type checking, but code with type hints allows:

  • IDEs (e.g. PyCharm) and static type checkers (e.g. mypy) to catch errors before runtime
  • to have a better, self-documented code

Basic Type Hinting

🚫

defsum_values(a, b):
returna+bsum_values(10, 3)
>>>13sum_values(10, "x")
>>>TypeError: unsupportedoperand type(s) for+: 'int'and'str'

defsum_values(a: int, b: int) ->int:
returna+bsum_values(10, "x") # IDE will highlight an error

Type Hinting & Multiple Types

Before Python 3.10:

fromtypingimportUniondefsum_values(a: Union[int, float], b: Union[int, float]) ->Union[int, float]:
# a and b can be either int or floatreturna+b

Starting Python 3.10:

defsum_values(a: int|float, b: int|float) ->int|float:
returna+b

Type Hinting & Containers

Before Python 3.10:

fromtypingimportDict, Listdefmake_list(a: str, b: str) ->List[str]:
# return type is a list of stringsreturn [a, b]
defmake_dict(k: str, v: str) ->Dict[str, str]:
# return type is a dict with string keys and string valuesreturn {k: v}
make_list("hello", "world")
>>> ['hello', 'world']
make_dict("hello", "world")
>>> {'hello': 'world'}

Starting Python 3.10:

defmake_list(a: str, b: str) ->list[str]:
return [a, b]
defmake_dict(k: str, v: str) ->dict[str, str]:
return {k: v}

Type Hinting & Generic Objects

Before Python 3.10:

fromdataclassesimportdataclassfromtypingimportList@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: List[City]) ->List[City]:
returnsorted(cities, key=lambdax: x.name)
cities= [
City(name='Madrid', country='ES'),
City(name='Berlin', country='DE'),
City(name='Edinburgh', country='UK')
]
sort_cities(cities)
>>> [City(name='Berlin', country='DE'), City(name='Edinburgh', country='UK'), City(name='Madrid', country='ES')]

Starting Python 3.10:

fromdataclassesimportdataclass@dataclassclassCity:
name: strcountry: strdefsort_cities(cities: list[City]) ->list[City]:
returnsorted(cities, key=lambdax: x.name)

Type Hinting and Local Variables

Not only limited to inputs/outputs of a function:

Before Python 3.10:

fromtypingimportListresult: List[str] = [] # not just a list of anything!

Starting Python 3.10:

result: list[str] = []

👷‍♂️ Operators


👯‍♀️ Double Comparison

🚫

ifvalue>0andvalue<100:

if0<value<100:

🧬 Pattern Matching

Added in Python 3.10. Similar to switch statements in other languages, on steroids:

defparse_command(command: str) ->str:
matchcommand.split():
case [action, direction]:
returnf"Parsed: {action=}, {direction=}"case ["help"]: return"Help message goes here"case _:
return"Wrong command, 2 words expected"parse_command("go north")
>>>"Parsed: action='go', direction='north'"parse_command("look up")
>>>"Parsed: action='look', direction='up'"parse_command("go")
>>>"Wrong command, 2 words expected"parse_command("help")
>>>"Help message goes here"

Alias matching with as, OR matching with | and conditional matching:

defparse_command(command: str) ->str:
matchcommand.split():
case ["go", ("north"|"south") asdirection]:
returnf"Going {direction}"case ["go", _]: return"Sorry, can't go there!"case (["pick", obj, "up"] | ["pick", "up", obj]) ifobjin ['shovel', 'rock']:
returnf"Picking up {obj}"case ["pick", _, "up"] | ["pick", "up", _]:
return"Sorry, can't pick this up!"case _:
return"Wrong command, 2 words expected"parse_command("go south")
>>>"Going south"parse_command("go left")
>>>"Sorry, can't go there!"parse_command("pick shovel up")
>>>"Picking up shovel"parse_command("pick phone up")
>>>"Sorry, can't pick this up!"

Adapting to different structure types:

fromdataclassesimportdataclassfromdatetimeimportdatetime@dataclassclassUser:
age: intdefget_age(user: dict|User) ->int:
matchuser:
caseUser(age):
returnagecase {"dob": {"age": int(age) |float(age)}}:
returnint(age)
case {"dob": dob}:
now=datetime.now()
dob_date=datetime.strptime(dob, "%Y-%m-%d %H:%M:%S")
returnnow.year-dob_date.year
get_age({"dob": "1966-04-17 11:57:01"})
>>>56get_age({"dob": {"date": "1957-05-20T08:36:09.083Z", "age": 64}})
>>>64get_age({"dob": {"age": 39.6}})
>>>39get_age(User(age=40))
>>>40

🍴 Ternary Operator

ifa==5:
result="Five!"else:
result="Not five..."

Shorter way to write the same:

result="Five!"ifa==5else"Not five..."

🦷 Walrus Operator :=

Added in Python 3.8:

value=123print(value)
>>>123# can be written as:print(value:=123)
>>>123value>>>123

😒 Can be fine, but not the most concise way:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
# get some stats on the list: length, sum, mean valuesnum_length=len(numbers)
num_sum=sum(numbers)
stats= {
"length": num_length,
"sum": num_sum,
"mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

✅ Doing the same with less lines of code:

numbers= [2, 8, 0, 1, 1, 9, 7, 7]
stats= {
"length": (num_length:=len(numbers)), "sum": (num_sum:=sum(numbers)), "mean": num_sum/num_length
}
>>>stats
{'length': 8, 'sum': 35, 'mean': 4.375}

🛂 == vs is

  • ==: do two objects have the same contents?
  • is: are two objects the same thing (point to the same address in memory)?
a= [1, 2, 3]
b= [1, 2, 3]
a==b>>>True
id(a) # Python id of object a>>>4435362944id(b) # Python id of object b>>>4435377344aisb# same as id(a) == id(b)>>>Falsea=baisb>>>True

ℹ️ As a consequence:

a= [1, 2, 3]
b= [1, 2, 3]
a[0] =4print(a, b)
>>> [4, 2, 3] [1, 2, 3]
a=b# make a point to the same object as b, not copying contents of b!a[0] =4# also changes b now as a and b point to the same address in memoryprint(a, b)
>>> [4, 2, 3] [4, 2, 3]

Copying an Object

🚫 Looks cryptic, and only works for lists but not for e.g. dicts:

a= [1, 2, 3]
b=a[:]
a==b>>>Trueaisb>>>False

a= [1, 2, 3]
b=a.copy()
a==b>>>Trueaisb>>>False

ℹ️ There's only one global None object

c=Noned=Nonec==d>>>Truecisd# c and d and not "copies" of None, they point to it>>>True

🧮 Named Parameters

defprint_issue_info(issue_id: str, issue_title: str)
print(f"Issue id: {issue_id}, title: {issue_title}")

😒 Can be fine:

print_issue_info("1234", "Create new thing")
>>>"Issue id: 1234, title: Create new thing"

✅ More explicit and human-readable:

print_issue_info(issue_id="1234", issue_title="Create new thing")
>>>"Issue id: 1234, title: Create new thing"print_issue_info(issue_title="Create new thing", issue_id="1234")
>>>"Issue id: 1234, title: Create new thing"

🏀 Practical Examples


📁 Reading/Writing Files

🚫 Handle errors yourself:

f=open('data.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()

✅ Use a context managerwith:

withopen("data.txt", "r") asf:
data=f.read()
withopen("data2.txt", "w") asf:
f.write(data)

🅾️ Loading/Exporting JSON

json is a built-in Python module:

importjsondata= {"a": 123, "b": None}
data_json=json.dumps(data) # dumps = dump stringprint(data_json)
>>> {"a": 123, "b": null}
data_parsed=json.loads(data_json) # loads = load stringprint(data_parsed==data)
>>>True

Reading JSON Files

$ cat file.json
{
"a": {"b": 123}
}
importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123

🌍 HTTP Requests

✅ Use the requests library:

importrequestsurl='https://api.github.com/some/endpoint'headers= {'Authentication': 'Bearer mytoken'}
r=requests.get(url, headers=headers)
print(r.status_code)
>>>200print(r.json())
>>> {"status": "OK", "message": "hi from the API"}

🔨 Tools


🔁 REPL

REPL = read-eval-print loop

Can be used to quickly try things out in a terminal:

$ python3
Python 3.10.6 (main, Aug 11 2022, 13:49:25) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license"for more information.
>>> sum(range(0, 10))
45

🐞 Debugging

breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):

value=123breakpoint()
(Pdb) value>>>123

🏁 Outro: Key Advice

  • Write as short and lean code as possible
  • Use the most recent version of Python
  • Try ideas with REPL quickly
  • Use type hinting
  • Know your basic data structures
  • Use list & dict comprehensions
  • Use f-strings
  • Use data classes
  • RealPython.com is a great source of guide on specific topics (example)

🙇‍♂️ Thank You!

About

Slides for tech talk in Camunda on Python tips and tricks (Sep 2022)

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors