Python “tricks” I can not live without

By Sebastian Opałczyński

Image for post
Image for post
It’s python

There is plenty of similar articles on the web, but I just thought that I can add my perspective on the topic, and make the “tricks” list more complete. The code fragments used here are somehow crucial to my workflow and I am reusing them over and over again.

Sets

It’s often the case that developers tend to forget that python has set datatype and trying to use lists for everything. What is set? Well, long story short:

A set is an unordered collection with no duplicate elements.

If you get familiar with sets and their logic it can solve a lot of problems for you. For example — how to get all of the unique letters used in a word?

myword = "NanananaBatman"
set(myword)
{'N', 'm', 'n', 'B', 'a', 't'}

Boom. Problem solved, but to be honest it’s from the official python documentation, so nothing surprising.

What about this? Get a list of items without element repetition?

# first you can easily change set to list and other way aroundmylist = ["a", "b", "c", "c"]# let's make a set out of itmyset = set(mylist)# myset will be:
{'a', 'b', 'c'}
# and, it's already iterable so you can do:for element in myset:

print(element)

# but you can also convert it to list again:
mynewlist = list(myset)
# and mynewlist will be:
['a', 'b', 'c']

As you can see the repeating “c” is not the case anymore. Only one thing you should be aware of is that the order of elements between mylist and mynewlist can be different:

mylist = ["c", "c", "a", "b"]mynewlist = list(set(mylist))# mynewlist is:

['a', 'b', 'c']

# as you can see it's different order;

But! We can go deeper.

Imagine the case that you have a one to many relation between some entities, to make it more concrete — user and permissions; usually, it’s the case that one user can have multiple permissions. Now imagine that someone wants to modify multiple permissions — adding some and removing some at the same time, how to tackle this problem?

# this is the set of permissions before change;
original_permission_set = {"is_admin", "can_post_entry", "can_edit_entry", "can_view_settings"}
# this is new set of permissions;
new_permission_set = {"can_edit_settings", "is_member", "can_view_entry", "can_edit_entry"}
# now permissions to add will be:new_permission_set.difference(original_permission_set)# which will result:

{'can_edit_settings', 'can_view_entry', 'is_member'}

# As you can see can_edit_entry is in both sets; so we do not need
# to worry about handling it
# now permissions to remove will be:original_permission_set.difference(new_permission_set)# which will result:

{'is_admin', 'can_view_settings', 'can_post_entry'}

# and basically it's also true; we switched admin to member, and add
# more permission on settings; and removed the post_entry permission

Long story short — don't be afraid of sets as they can solve you a lot of pain, more on sets you can find in official python docs.

Calendar fun

It’s very often when you develop something that heavily depends on dates and periods of time like months or weeks — you are interested in some information, like what is the last day of the month in a given year. It seems simple, but believe me that properly handling the date and time is an extremely hard topic, and I like to say that calendar implementation has very poor grooming, and it’s a nightmare, cause of tons of edge cases.

So — how to find the last day of the month?

import calendarcalendar.monthrange(2020, 12)# will result:
(1, 31)
# BUT! you need to be careful here, why? Let's read the documentation:help(calendar.monthrange)# Help on function monthrange in module calendar:
# monthrange(year, month)# Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for

# year, month.

# As you can see the first value returned in tuple is a weekday, # not the number of the first day for a given month; let's try

# to get the same for 2021

calendar.monthrange(2021, 12)
(2, 31)
# So this basically means that the first day of December 2021 is Wed# and the last day of December 2021 is 31 (which is obvious, cause

# December always has 31 days)

# let's play with February
calendar.monthrange(2021, 2)(0, 28)calendar.monthrange(2022, 2)(1, 28)calendar.monthrange(2023, 2)(2, 28)calendar.monthrange(2024, 2)(3, 29)calendar.monthrange(2025, 2)

(5, 28)

# as you can see it handled nicely the leap year;

As for the beginning of the month, this is really simple — it always starts from 1st :)

How you can use the information that the month is starting on a given weekday? You can easily determine weekday for any day:

calendar.monthrange(2024, 2)
(3, 29)
# means that February 2024 starts on Thursday# let's define simple helper:

weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

# now we can do something like:
weekdays[3]
# will result in:
'Thursday'
# now simple math to tell what day is 15th of February 2020:
offset = 3 # it's the first value from monthrange
for day in range(1, 29):
print(day, weekdays[(day + offset - 1) % 7])
1 Thursday2 Friday3 Saturday4 Sunday...18 Sunday19 Monday20 Tuesday21 Wednesday22 Thursday23 Friday24 Saturday...28 Wednesday

29 Thursday

# which basically makes sense;

It’s maybe not super “production” ready example, as weekday can be easily found using datetime module:

from datetime import datetime
mydate = datetime(2024, 2, 15)datetime.weekday(mydate)# will result:

3

# or:datetime.strftime(mydate, "%A")
'Thursday'

Anyway, there’s some fun in the calendar module definitely, which is good to know:

# checking if year is leap:calendar.isleap(2021) # False
calendar.isleap(2024) # True
# or checking how many days will be leap days for given year span:calendar.leapdays(2021, 2026) # 1
calendar.leapdays(2020, 2026) # 2
# read the help here, as range is: [y1, y2), meaning that second
# year is not included;
calendar.leapdays(2020, 2024) # 1

Enumerate has a second argument

Yes, it has a second argument. It’s funny, but some really experienced developers are not aware; You should check the examples:

mylist = ['a', 'b', 'd', 'c', 'g', 'e']for i, item in enumerate(mylist):
print(i, item)
# Will give:0 a1 b2 d3 c4 g

5 e

# but, you can add a start for enumeration:for i, item in enumerate(mylist, 16):
print(i, item)
# and now you will get:16 a17 b18 d19 c20 g

21 e

It’s a simple start point of where enumeration should begin. It can help you in situations where you are working on some kind of offsets logic.

Handling if-else logic

It happens often that you need to handle a lot of different logic depending on the condition, an inexperienced developer will end up in something like this:

OPEN = 1IN_PROGRESS = 2

CLOSED = 3

def handle_open_status():
print('Handling open status')
def handle_in_progress_status():
print('Handling in progress status')
def handle_closed_status():
print('Handling closed status')
def handle_status_change(status): if status == OPEN: handle_open_status() elif status == IN_PROGRESS: handle_in_progress_status() elif status == CLOSED:

handle_closed_status()

handle_status_change(1) # Handling open statushandle_status_change(2) # Handling in progress status

handle_status_change(3) # Handling closed status

It doesn’t look too bad, but I’ve seen codebases with 20 and more conditions like this.

How it should be handled then?

from enum import IntEnum
class StatusE(IntEnum): OPEN = 1 IN_PROGRESS = 2

CLOSED = 3

def handle_open_status():
print('Handling open status')
def handle_in_progress_status():
print('Handling in progress status')
def handle_closed_status():
print('Handling closed status')
handlers = { StatusE.OPEN.value: handle_open_status, StatusE.IN_PROGRESS.value: handle_in_progress_status, StatusE.CLOSED.value: handle_closed_status

}

def handle_status_change(status): if status not in handlers: raise Exception(f'No handler found for status: {status}') handler = handlers[status]

handler()

handle_status_change(StatusE.OPEN.value) # Handling open statushandle_status_change(StatusE.IN_PROGRESS.value) # Handling in progress statushandle_status_change(StatusE.CLOSED.value) # Handling closed status

handle_status_change(4) # Will raise the exception

It’s a common pattern that can be used in python, generally, it makes the code look a bit cleaner — especially where your main handling method is huge and have a lot of conditions to handle.

Enum module

I’ve scratched the topic in the above paragraph a bit, let’s dig deeper.

enum module provides utilities of handling enumerations, the most interesting are: Enum, IntEnum — let’s dig a little:

from enum import Enum, IntEnum, Flag, IntFlag
class MyEnum(Enum): FIRST = "first" SECOND = "second"

THIRD = "third"

class MyIntEnum(IntEnum): ONE = 1 TWO = 2

THREE = 3

# Now we can do things like:MyEnum.FIRST # <MyEnum.FIRST: 'first'># it has value and name attributes, which are handy:MyEnum.FIRST.value # 'first'
MyEnum.FIRST.name # 'FIRST'
# additionally we can do things like:MyEnum('first') # <MyEnum.FIRST: 'first'>, get enum by value
MyEnum['FIRST'] # <MyEnum.FIRST: 'first'>, get enum by name

With IntEnum it’s similar — but there are some differences:

MyEnum.FIRST == "first" # False# butMyIntEnum.ONE == 1 # True# to make first example to work:MyEnum.FIRST.value == "first" # True

In mid-size codebases enum module is extremely helpful to manage your constants in the project.

The localization of enum can be a little bit tricky, but it’s doable, let me show you quickly how I am handling this in django:

from enum import Enumfrom django.utils.translation import gettext_lazy as _
class MyEnum(Enum): FIRST = "first" SECOND = "second"

THIRD = "third"

@classmethod def choices(cls): return [ (cls.FIRST.value, _('first')), (cls.SECOND.value, _('second')), (cls.THIRD.value, _('third'))

]

# And later in eg. model definiton:some_field = models.CharField(max_length=10, choices=MyEnum.choices())

IPython FTW

ipython means interactive python and it’s a command shell for interactive computing, it’s like a python interpreter, but with batteries included or differently put: on steroids.

To use ipython you need to install it:

pip install ipython

And later instead of the typical use ofpython command to go into the interpreter, use insteadipython

# you should see something like this after you start:
Python 3.8.5 (default, Jul 28 2020, 12:59:40) Type 'copyright', 'credits' or 'license' for more information

IPython 7.18.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]:

It supports system commands, like: ls or cat the tab key will show hints, which makes the session with interactive programming much more pleasant. You can also use up/down arrows to search for recent commands, generally, you can do a lot of things — refer to the official documentation of ipython. I especially recommend getting familiar with magic functions, using them you can save, share the code you used, measure the execution time, and much more.

That would be it for now. Hope you will find something interesting here.