loading...
Cover image for Make Your Code Great, Python Style

Make Your Code Great, Python Style

livecodestream profile image Juan Cruz Martinez Originally published at livecodestream.dev on ・8 min read

Python is a great programming language that offers us amazing tools to make our code more readable, concise, and cool. Today I'd like to talk about ways to write more Pythonic code, we will cover some great tricks that will improve the quality of your code. Let's start...


Using Unpacking

Python allows a tuple (or list) of variables to appear on the assignment side of an operation. This allows us to simplify our code, making it more readable.

Let's start with an example of unpacking tuples:

>>> a, b, c = (1, 2, 3)
>>> a
1
>>> b
2
>>> c
3

Easy enough, more than one variable can be on the left side of our assignment operation, while the values on the right side are assigned one by one to each of the variables. Just be aware that the number of items on the left side should equal the number of items on the right, or you may get a ValueError like this:

>>> a, b = 1, 2, 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)
>>> a, b, c = 1, 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)

However, since Python is awesome, there are ways to prevent this from happening, you can do something like:

>>> a, *b, c = 1, 2, 3, 4, 5, 6
>>> a
1
>>> b
[2, 3, 4, 5]
>>> c
6

What did just happen? When we use the * operator in this context, it will extend the unpacking functionality to allow us to collect or pack multiple values in a single variable. Exactly all the once which were not used by the expression. It's awesome, just remember you can have only one * operator in the assignment to avoid SyntaxError.

>>> a, *b, c, *d = 1, 2, 3, 4, 5, 6
  File "<stdin>", line 1
SyntaxError: two starred expressions in assignment

but you can also unpack lists:

>>> a, b, c = ['a', 'b', 'c']
>>> a
'a'

or strings....

>>> a, b, c = 'def'
>>> a
'd'

actually, you can use any iterable in this way. But let me show you one more cool thing before we jump to the next topic:

>>> a = 1
>>> b = 2
>>> a, b = b, a
>>> a
2
>>> b
1

Isn't that the most beautiful variable swap ever?


Checking against None

The None keyword in Python is used to define a null value, or no value at all. Unlike other languages, None in Python is a datatype of its own (NoneType) and only None can be None. Let's see it in examples how does it work:

x = None

>>> type(x)
<class 'NoneType'>

>>> x == 0
False

>>> x == False
False

If you want to check if a variable is actually None you could do something like this:

>>> x == None
True

And it is valid, however there's a more Pythonic way of doing it:

>>> x is None
True

>>> x is not None
False

It does the same job, however, it looks more human.


Iterating

Another great example where python excels, iterations in Python can be very elegant, or terribly Unpythonic (if that word even exists).

How would you try to loop in Python if you are coming maybe from JS or C? Happened to me at first

>>> x = [1, 2, 3, 5, 8, 11]
>>> for i in range(len(x)):
...     print(x[i])
... 
1
2
3
5
8
11

Then I learned some other options:

>>> x = [1, 2, 3, 5, 8, 11]
>>> for i in x:
...     print(i)
... 
1
2
3
5
8
11

but maybe what you want is to iterate in reverse order, well, you can do something like:

>>> x = [1, 2, 3, 5, 8, 11]
>>> for item in x[::-1]:
...     print(item)
... 
11
8
5
3
2
1

That's pretty good, but it still looks weird, doesn't look like human, maybe there's another way:

>>> for item in reversed(x):
...     print(item)
... 
11
8
5
3
2
1

Now looks beautiful! but what if we need the index and the item value? We had all that in our first attempt and now we seem to have lost it. No worries, there are Pythonic ways to do it as well:

>>> x = [1, 2, 3, 5, 8, 11]
>>> for i, item in enumerate(x):
...     print(i, item)
... 
0 1
1 2
2 3
3 5
4 8
5 11

But sometimes we have more than 1 array we want to iterate over, how would we do that? We can use our packing/unpacking friends:

>>> names = ['Juan', 'Gera', 'Martin']
>>> ages = [33, 30, 36]
>>> for person in zip(names, ages):
...     print(person)
... 
('Juan', 33)
('Gera', 30)
('Martin', 36)

And if we want to access each value individually:

>>> names = ['Juan', 'Gera', 'Martin']
>>> ages = [33, 30, 36]
>>> for person in zip(names, ages):
...     name, age = person
...     print(name, age)
... 
Juan 33
Gera 30
Martin 36

Or even better:

>>> names = ['Juan', 'Gera', 'Martin']
>>> ages = [33, 30, 36]
>>> for name, age in zip(names, ages):
...     print(name, age)
... 
Juan 33
Gera 30
Martin 36

Sometimes we need to iterate over objects, and Python allows us to do that easily:

>>> obj = {'name': 'Juan', 'age': 33}
>>> for k in obj:
...     print(k, obj[k])
... 
name Juan
age 33

But we can also get both the key and the value using the .items() method of the object:

>>> obj = {'name': 'Juan', 'age': 33}
>>> for k, v in obj.items():
...     print(k, v)
... 
name Juan
age 33

Objects also offer methods like .keys() and .values() which depending on your use case can be very helpful.

Note that Python offers us many ways to iterate over things, sometimes a looping over a range() is what we need and that's perfectly fine, but some other alternatives can be clearer for developers to read, and we should use them when possible.


Avoid Mutable Optional Arguments

As many other languages Python offers us the possibility to have optional arguments, and as they can be very handy, they can also bring some unexpected behaviors. Let's look at the following example:

>>> def add_value(value, list=[]):
...     list.append(value)
...     return list
...

So far so good, we have a function called add_value, which will add a value to the list every time we call the function, and will return the list at the end. The list is a parameter which is optional. Let's now call our function and see how it behaves:

>>> add_value(5)
[5]

Perfect, our optional parameter is working, and we get as a result a list with a single value. Let's try adding some more:

>>> add_value(5)
[5]
>>> add_value(8)
[5, 8]
>>> add_value(13)
[5, 8, 13]

Wait... what? That doesn't look right, however is the actual result, and once we explain it, it will make sense.... or maybe not... let's see.

When we define our function Python generates an instance of the default value, and this instance is then used every time the optional value is not provided. This is a big issue for our use case, as the list will keep growing even though that's not what we we need in this particular case. So how we go about fixing it? We could do something as follows:

>>> def add_value(value, list=None):
...     if not list:
...             list = []
...     list.append(value)
...     return list
...

Here is something that looks strange in Python and I don't like it so much, though that feature we now detected as an issue in many cases can be super useful, we just need to be aware of it and use it wisely.


Properties vs Getters and Setters

WARNING: This topic may be cause controversy among Java developers 😛. Out of all jokes, it's very tempting if you are a Java developer or coming from C++ to try doing something like the following:

>>> class Person:
...     def get_name(self):
...             return self.__name
...     def set_name(self, name):
...             self.__name = name
... 
>>> person = Person()
>>> person.set_name('Juan')
>>> person.get_name()
'Juan'

Now, even if there's nothing wrong with that, it's not the Python way. Before I present you the Python way, let me get my gloves 🥊

>>> class Person:
...     @property
...     def name(self):
...             return self.__name
...     @name.setter
...     def name(self, value):
...             self.__name = value
... 
>>> person = Person()
>>> person.name = 'Juan'
>>> person.name
'Juan'

I'm ready for the fight now....😛


Protected and Private attributes, but not really....

"Protected" or "Private" instance variables that cannot be accessed except from inside an object don't exist in Python, however there is a convention used by all Python developers to specify these attributes.

>>> class Test:
...     def __init__(self, *args):
...             self.x, self._y, self.__z = args
... 
>>> test = Test(1, 2, 4)

If we now try to access the x property from outside the class block we get the actual value of x. And this is correct and a good practice

>>> test.x
1

We can try the same thing with _y:

>>> test._y
2

And we get the result, however this is consider bad practice as the attribute starts with _ and was intended by the developers of the class not to be accessible from outside.

But what happens now with __z:

>>> test.__z
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute '__z'

In this case we get an error, great, however, it's still possible to access the attribute, we just need to add some magic to our code:

>>> test._Test__z
4

If we prepend the attribute name with _classname we can still access the value, but it's terrible wrong to do so.

According to the Python docs:

a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API (whether it is a function, a method or a data member). It should be considered an implementation detail and subject to change without notice.

Any identifier of the form spam (at least two leading underscores, at most one trailing underscore) is textually replaced with _classnamespam, where classname is the current class name with leading underscore(s) stripped


Use Context Managers to Handle Resources

When working with resources, such as files, database connections, etc, that we need to handle the code for successfully close or release the resource, and it's very common to see something like the following:

>>> my_file = open('filename.txt', 'w')
>>> # read my_file or do something with it
>>> my_file.close()

This code is right, unless something happens in between, what if an error occurs, how can we make sure that the file gets always closed? Here is where Context Managers enter into play:

>>> with open('filename.txt', 'w') as my_file:
...     # do something with `my_file`

That's a much safer way to do it!


Summary

Python is a very simple and elegant language to work with. It's simplicity makes it very popular among students or people learning to code, however, it's very important to write proper Python code. I hope that after reading the article you have a few ideas into what's the Pythonic way of writing code, and that you can research for many more.

I hope you enjoyed it!


If you like the story, please don't forget to subscribe to our newsletter so we can stay connected: https://livecodestream.dev/subscribe

Posted on by:

livecodestream profile

Juan Cruz Martinez

@livecodestream

I'm an entrepreneur, developer, author, speaker, and doer of things. Email List: https://livecodestream.dev/subscribe

Discussion

markdown guide