- 🐍 Python: Tips & Tricks
- 🪷 Zen of Python
- 📝 Python History
- 🎻 String Formatting
- 🌱 Basic Data Structures
- 🧑🔧 Data Structures: Examples
- 📦 Values Unpacking
- 🌾 Data Classes
- 🦆 Type Hinting
- 👷♂️ Operators
- 🧮 Named Parameters
- 🏀 Practical Examples
- 🔨 Tools
- 🏁 Outro: Key Advice
- 🙇♂️ Thank You!
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!
- 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%)
There are 4 ways to do that in Python, with a single preferred one.
🚫 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'🚫
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}🚫
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)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'}- Lists
- Strings
- Dicts
- Tuples
- Sets
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🚫 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🚫
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)
>>>5Done with so-called 🍣 sushi-operator ([::]):
array[<start_index>:<stop_index>:<step>]
start_index=0if not specifiedstop_index=len(array)if not specified- it's exclusive:
stop_indexvalue is not included in the slice result
- it's exclusive:
step_index=1if 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[:]
>>>TrueSlicing 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]🚫 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}")
>>>TrueFormula: [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]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]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'}]🚫 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")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]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]🚫 Is substring in string:
"restaurant".find("aura") >-1>>>True"waterfall".find("fun") >-1>>>False✅
"aura"in"restaurant">>>True"fun"in"waterfall">>>Falsedata=' 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? 'Added in Python 3.9:
print("INFRA-123".removeprefix("INFRA-"))
>>>'123'print("INFRA-123".removesuffix("-123"))
>>>'INFRA'string='lorem ipsum dolor sit amet'tokens=string.split(" ")
tokens>>> ['lorem', 'ipsum', 'dolor', 'sit', 'amet']Dict is key-val storage:
data= {'city': 'Berlin', 'country': 'DE', 'areas': ['Moabit', 'Mitte', 'Westend']}
data['areas'][1]
>>>'Mitte'data['city'] ='Bielefeld'data['city']
>>>'Bielefeld'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']🚫
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>>>TrueFormula: {key: val for item in iterable}
data= {x: x.upper() forxin ['apple', 'banana']}
data>>> {'apple': 'APPLE', 'banana': 'BANANA'}data= {'a': 123, 'b': 456}
data['a']
>>>123data['b']
>>>456data['c']
>>>KeyErrorexceptiondata= {'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")
🚫 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>>>Noneeurope= {'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'}Tuple is an immutable, ordered array of values:
data= (1, 2, 3)
data[1]
>>>2data[1] =10>>>TypeError: 'tuple'objectdoesnotsupportitemassignmentTuples can be implicit:
a=1, 2a[0]
>>>1# the same is:a= (1, 2)Set is a unordered array of unique values:
data= {1, 42, -1, 1}
>>> {1, 42, -1}
data[0]
>>>TypeError: 'set'objectisnotsubscriptableConverting 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]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🚫
ifoperation=="READ"oroperation=="WRITE":✅
ifoperationin ["READ", "WRITE"]:🚫 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🚫
data= ['one', 'two']
a=data[0]
b=data[1]
print(a)
>>>oneprint(b)
>>>two✅
data= ['one', 'two']
a, b=dataa>>>oneb>>>twoAlso 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🚫 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.0There'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
🚫
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 errorBefore 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+bStarting Python 3.10:
defsum_values(a: int|float, b: int|float) ->int|float:
returna+bBefore 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}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)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] = []🚫
ifvalue>0andvalue<100:✅
if0<value<100: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.yearget_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))
>>>40ifa==5:
result="Five!"else:
result="Not five..."Shorter way to write the same:
result="Five!"ifa==5else"Not five..."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}==: 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>>>Trueid(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]🚫 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>>>Truedefprint_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"🚫 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)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$ cat file.json
{
"a": {"b": 123}
}importjsondata=json.load(open("file.json")) # data will be a dictprint(data["a"]["b"])
>>>123✅ 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"}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))
45breakpoint() stops execution of the program at the given line and runs an interactive debugger (added in Python 3.7):
value=123breakpoint()
(Pdb) value>>>123- 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)