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.