Python tips and tricks you may not know

python

I will just throw some random tips and tricks in Python that I've found that I thought was really helpful but most people don't use much.

For my own convenience, all of the examples below is written on Python 3.9.6 cuz I have it on my machine rigth now.

String formatting: Please use the fking f-string! #

I'm still seeing a lot of these string-formatted code in the old-school way like these:

name = "Loc Mai"
job = "SRE"
print("Hello, my name is {fname} and I'm and {fjob}".format(fname="Loc Mai", fjob="SRE"))

OR

name = "Loc Mai"
job = "SRE"
print("Hello, my name is {0} and I'm and {1}".format(name, job))

The code above will print out "Hello, my name is Loc Mai and I'm an SRE"

Here is the shorter one with f-string that print out the same output:

name = "Loc Mai"
job = "SRE"
print(f"Hello, my name is {name} and I'm and {job}")

Clean, DRY, easier to read for human's eyes.

Decorator pattern #

This is a pattern where you simply write decorator functions those could wrap around the other functions and change some of their behaivours.

Let's say I want to calculate the time execution of a function when it ran:

from datetime import datetime

def timer(func):
def wrapper():
start = datetime.now()
func()
end = datetime.now()
print(f"Time execution for `{func.__name__}()`: {end - start}")
return wrapper

def hello_world():
print("Hello World!")

hello_world = timer(hello_world)

Now if we called the hello_world() function:

>>> hello_world()
Hello World!
Time execution for hello_world: 0:00:00.000038

So the code above could be written like this with the @timer decorator before the hello_world function and also add another function called bye_world():

from datetime import datetime
import time

def timer(func):
def wrapper():
start = datetime.now()
func()
end = datetime.now()
print(f"Time execution for `{func.__name__}()`: {end - start}")
return wrapper

@timer
def hello_world():
print("Hello World!")

@timer
def bye_world():
print("Bye bye World!")
time.sleep(2) # Sleep for 2 seconds after saying good bye

Now try the bye_world():

>>> bye_world()
Bye bye World!
Time execution for bye_world: 0:00:02.003713

More complicated use case like adding an ability to use arguments in the decorator could solve by wrapping the function like this:

from datetime import datetime
import time

def timer(threshold):
def wrap(func):
def wrapped_f(*args):
start = datetime.now()
func()
end = datetime.now()
print(f"Time execution for `{func.__name__}()`: {end - start} vs ")
print(f"Execution took: {(end - start).total_seconds()}")
print(f"Threshold: {threshold}")
return wrapped_f
return wrap

@timer(1)
def hello_world():
print("Hello World!")

List comprehension #

I use this a lot to create a new list based on existing lists with shorter syntax provided, let's say you have a list of dict about pokemon's profile:

pokemon_list = [
{
'name': 'Charizard',
'id': 6,
'type': ['Fire', 'Flying']
},
{
'name': 'Cinderace',
'id': 815,
'type': 'Fire'
},
{
'name': 'Pikachu',
'id': 25,
'type': 'Electric'
},
]

If I wanna filter and get the list of 'Fire' pokemon, I could do with just one line:

fire_pokemon_name_list = [pokemon['name'] for pokemon in pokemon_list if 'Fire' in pokemon['type']]
print(fire_pokemon_name_list)

# result: ['Charizard', 'Cinderace']

Type hinting #

Python go with dynamic typing model which is good if you didn't really care about type-safe. But if you were writing library code that the others would use, you could provide some typing hints for your users:

For example, we have library.py with greeting:

# library.py
def greeting(name: str) -> str:
return f'Hello {name}'

Pretending we are the users importing the library code:

from library import greeting

From the IDE, we could now see what parameter needed with what type, and the type of the returned value from the function greeting()

Bonus: Postional parameters and keyword parameters #

Never thought I should put this on the list until I have to debate with someone that using this properly can make their code better and they refused to use it, then later on see the useful of it.

Python provides a way to explicit define which parameters must be defined postionally, which must be defined by keywords.

Simple example is:

def simple_function(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)

The following calls will result in:

simple_function(10, 20, 30, d=40, e=50, f=60) # valid call, this will print out all the parameters

Explain: The syntax / to indicate that some function parameters must be specified positionally and cannot be used as keyword arguments, and the * indicate the parameters after it must be keywords.

So the following calls will be invalid to call:

simple_function(10, b=20, c=30, d=40, e=50, f=60)   # b cannot be a keyword argument
simple_function(10, 20, 30, 40, 50, f=60) # e must be a keyword argument

Moving on with that, if we were unsure about the number of arguments to pass in the functions, we could use *args and **kwargs instead.

By that, you could add any number of arguments into the function dynamically, for example we have the following adder which allow to add any positional parameters:

def adder(*num):
sum = 0

for n in num:
sum = sum + n

print("Sum of all:",sum)

adder(3,5)
adder(4,5,6,7)
adder(1,2,3,5,6)

The case I had the debate was how to write a custom Graph helper that extends and return the basic Graph class

import grafanalib.core as Graph

def CustomGraph(data_source, title, expressions, **kwargs) -> Graph:
prefix='custom'
return Graph(
title=f"{prefix}-{title}",
dataSource=data_source,
targets=targets,
**kwargs
)

.
.
.
.
Well, that's it for now, happy coding.