The ultimate guide to writing better Python code | by Ari Joury | Towards Data Science

Despite its downsides, Python remains the king of today’s programming world. Its versatility, beginner-friendliness, and huge community are just a few factors that have contributed to its tremendous growth over the last few years. And even though newer languages like Rust, Go, and Julia are gaining traction, Python’s reign will likely carry on over the next few years.
Whether you’re a beginner or a seasoned engineer, you’re probably dealing with Python on a regular basis. In this context, it makes perfect sense to up your game a little bit.
Developers don’t code one thing and then leave it untouched for years to come. Therefore, good code isn’t only about speed and efficiency. It’s also about readability and maintainability.
Like with everything in life, there is no silver bullet to boost your code to stellar-performance. There are a few quick fixes, like efficient functions and handy automations. To really make your code shine, however, you’ll also need to adopt long-term habits like keeping your code as simple as possible and being consistent with your style.
towardsdatascience.com
From processing input to printing messages on the screen, strings are a vital part of any programming language. In Python, there are three ways of formatting strings: the original %s-method, the str.format(), and, since Python 3.6, f-strings.
The good old %s-method is fine when you’re dealing with short expressions. All you need to do is mark the places where you’d like to insert your string with %s, and then reference these strings after the statement. Like this:
>>> name = "Monty">>> day = "Tuesday">>> "Happy %s, %s. Welcome to Python!" % (day, name)'Happy Tuesday, Monty. Welcome to Python!'
This is all well and good, but it gets messy when you’re dealing with a lot of strings at a time. That’s why Python introduced str.format() from version 2.6 on.
Essentially, the %s symbols are replaced by curly braces. Like so:
>>> "Happy {}, {}. Welcome to Python!".format(day, name)
The real perk of str.format() is that you’re now able to define objects and then reference them in your formatting statement:
>>> greeting = {'name': 'Monty', 'day': 'Tuesday'}>>> "Happy {day}, {name}. Welcome to Python!".format(day=greeting['day'], name=greeting['name']) 
This is a huge improvement, but f-strings are even more straightforward. Consider this:
>>> name = "Monty">>> day = "Tuesday">>> f"Happy {day}, {name}. Welcome to Python!"
This is even easier! What’s even nicer about f-strings, though, is that you can pass functions in them:
>>> f"Happy {day}, {name.upper()}. Welcome to Python!"'Happy Tuesday, MONTY. Welcome to Python!'
One last upside of f-strings is that they’re faster than the two other methods. Since they’re very straightforward, it makes perfect sense to start using them from now onwards.
List comprehensions are a neat way of making your code more concise while maintaining readability. Consider this piece of code, where we’re trying to store the first few square numbers in a list:
>>> square_numbers = [0, 1, 2, 3, 4]>>> for x in range(5):...     square_numbers.append(x**2)...>>> square_numbers[0, 1, 2, 3, 4, 0, 1, 4, 9, 16]
This is quite clunky, and we’ll have to delete the first few items from the list again. Also, we still have the variable x floating around and taking up memory. With a list comprehension, this becomes a lot more concise:
>>> square_numbers = [x**2 for x in range(5)]>>> square_numbers[0, 1, 4, 9, 16]
The syntax is fairly easy:
new_list = [expression for item in iterable (if conditional)]
So the next time you’re about to make a list with a for loop, and perhaps an if statement, think again. You could save a few lines of code every time.
Initializing lists in Python is easy. But you can up your game by using the * operator, for example like this:
>>> four_nones = [None] * 4>>> four_nones[None, None, None, None]
If you want to create a list of lists, you can use a list concatenation:
>>> four_lists = [[] for __ in range(4)]>>> four_lists[[], [], [], []]
This comes handy when you’re trying to initialize long lists, or lists of lists. Not only is it easier to read and to debug, but you’re also less likely to make typos in individual elements of the list.
Consider a list a where we want to delete all items that are equal to bar. We can use a for-loop to get that done:
>>> a = ['foo', 'foo', 'bar', 'bar']>>> for i in a:...     if i == 'bar':...             a.remove(i)...>>> a['foo', 'foo', 'bar']
But one bar is remaining! What happened?
Well, when i was in positions 0 and 1, it correctly recognized that those elements need not be removed. At i = 2, it deleted bar. Now the second bar moved to position 2. But now the loop has already ended because there is no more element at i = 3, therefore the second bar remains.
In short, don’t use for loops when you’re deleting items from a list. Although you can avoid this kind of mistake with a few fixes, they’re just very prone to errors. Instead, the list comprehension comes in handy again:
>>> foos = [value for value in a if value != 'bar']>>> foos['foo', 'foo']
Using dict.get() to access elements of a dictionary is clunky and error-prone. Consider this example:
>>> d = {'hello': 'world'}>>> if d.has_key('hello'):...    print d['hello']    # prints 'world'>>> else:...    print 'default_value'
It’s a lot more concise if you use dict.get():
>>> d.get('hello', 'default_value') # prints 'world'>>> d.get('thingy', 'default_value') # prints 'default_value'
Or, if you want it even shorter, you can write:
>>> if 'hello' in d:...    d['hello']
What do you do when you have a list but you don’t need all arguments? You can use _ for that:
>>> numbers = [1, 2, 3]>>> a, b, _ = numbers>>> a1
The underscore is conventionally used to tell the programmer that this variable is not worth remembering. While Python does remember the variable, using this convention adds to the readability of your code.
If you have a long list, but you’re only interested in the first or last few variables, you can do this:
>>> long_list = [x for x in range(100)]>>> a, b, *c, d, e, f = long_list>>> e98
A variable beginning with * can hold as any number of elements. In this example, we’ve created a list going from 0 to 99, and unpacked the first two and last three elements. Specifically, the next-to-last element, e, is 98, which we’d expected.
You can also use the * operator to pass any number of arguments to a function:
>>> def printfunction(*args):...    print(args)>>> printfunction(1,2,3,4,5,6)(1, 2, 3, 4, 5, 6)
Finally, you can use the ** operator to pass entire dictionaries to a function:
>>> def myfriendsfunction(name, age, profession):...     print("Name: ", name)...     print("Age: ", age)...     print("Profession: ", profession)>>> friendanne = {"name": "Anne", "age": 26, "profession": "Senior Developer"}>>> myfriendsfunction(**friendanne)Name:  AnneAge:  26Profession:  Senior Developer
These are quick and easy ways to access variables without too much fuss. Keep them in mind!
The traditional way of opening files implies that you need to close them again:
>>> newfile = open("file.txt")>>> data = newfile.read()>>>     print(data)>>> newfile.close()
You can save a line and the potential to introduce bugs by using with open:
>>> with open('file.txt') as f:>>>     for line in f:>>>         print line
This way, even if an exception is raised within the with block, the file will be closed again.
If you’d like to see what a function does, just use help(). This is useful if you know in principle what you’re doing but aren’t too sure about the details of a function.
You can also make your life easier by adding a help-message to your own functions. That’s quite easy to pull off:
>>> def mynewfunction(arg1, arg2):...     """...     This function does something really cool....     Hope that helps!...     """>>> help(mynewfunction)This function does something really cool.Hope that helps!
This help-message, a docstring, is also useful as a documentation of your code. In this example, you’d obviously need to add a body to the function and flesh the message out a bit. But you get the message.
If you’re not quite sure about how an object works, dir() can help you. It returns the attributes and methods of whichever function, module, list, dictionary, or other object you wish to learn about.
The dir() function is available from Python 2.7 onwards. So for most Python users, using this might be a nice option.
If your program is sluggish and you think it has to do with memory issues, you can check through your objects with getsizeof() to understand where the culprit is. That is fairly easy:
>>> import sys>>> x = [1, 2, 3, 4, 5]>>> sys.getsizeof(x)112
Since x is a vector made of five 24-bit integers on my machine, it makes perfect sense that it takes up 112 bit of memory. Note that this number varies a bit, depending on your machine, architecture, and Python version.
As mentioned above, adding docstrings to all your module, function, class, or method definitions is an extremely good idea.
To make your code as maintainable and readable as possible, you’ll want to choose one docstring style and stick to that. If you’re collaborating on a project, make sure that you’ve decided on one style from the beginning. This makes it a lot easier to read and to add to the documentation later on.
Most software projects have a three-digit version number, for example “Python 3.8.4". The system is straightforward — if you’re making minor changes or bug fixes, you change the last number, for larger changes you change the middle number, and for rather fundamental changes you change the first number.
Even if your project is small, there are a few advantages to keeping all the old copies and numbering them. If you’re trying to make a bug fix but you end up breaking something, you can always go back to the latest version. And if your project isn’t backward-compatible, keeping old copies is vital. In addition, this numbering scheme creates a neat change log that you can refer to later.
If you’re collaborating or working on an open-source project, this is a must: Put your code on Github, Gitlab, or wherever else you’d like your code to live. This way it’s accessible by anyone at any time.
Also, make sure you git it often. Not only does that keep everyone up to date; it’s also a sign that your code is being maintained and still relevant.
Even if you’re using docstrings and consistently numbering your versions, that won’t capture all the changes you’re making. In order to keep the details safe, it’s a good idea to have a separate document where all changes are explained in detail.
Even though this will hardly be relevant to the end user, your change log will be a valuable document for yourself and for your colleagues later on. The human mind forgets fast, and things that are completely obvious to you now are often pretty opaque in two weeks.
Have you noticed anything about the examples above? The names of functions and variables almost always quite long. That’s no coincidence.
It may seem superfluous to use so many characters for a bit of code, but it’s a lot better for reading the code. Besides, it doesn’t take that much longer to code — most editors have autocomplete anyway.
Perhaps you’re one of those developers whose code runs but is sloppily formatted. While that might not sound so tragic, it creates a heap of problems down the road. Not to mention the colleagues that try to read your code...
So do yourself a favor and format your code with PEP 8, the official Python style guide. Don’t worry, you don’t need to learn every detail and refactor each piece of your code. You can automatically check whether your code conforms with pycodestyle. In your shell, install the package, then run it over your Python file:
$ pip install pycodestyle$ pycodestyle my_file.py
This signals where you need to make adjustments so your file conforms to PEP 8. If you want to get your file to conform automatically, you can use autopep8:
$ pip install autopep8$ autopep8 --in-place my_file.py
There’s an ongoing debate about whether comments are good or bad. Some programmers believe that good code is so readable that you don’t need any comments. Others would like you to comment every line.
Unless you are a total purist, you’re at least commenting from time to time. In any case, make sure you keep your comments short and to-the-point. I like to think about what somebody would like to read if they’d never seen my code but had a look at it now because they wanted to change something. That tends to work pretty well.
In principle, you could split a multi-line expression with a \ at the end of each line, like so:
>>> big_string = """This is the beginning of a really long story. \... It's full of magicians, dragons and fabulous creatures. \... Needless to say, it's quite scary, too."""
However, this is rather error-prone because it stops working if you put a character behind the \, even if it’s just a whitespace. It’s better to split it like so:
>>> big_string = (... "This is the beginning of a really long story. "... "It's full of magicians, dragons and fabulous creatures. "... "Needless to say, it's quite scary, too."... )
Most of the time, however, a multi-line expression is a sign that you’re overcomplicating things.
This should be a no-brainer, but it’s astounding how many junior developers still write monolithic code. But nobody reads a thousand lines of code...
So make sure that you define functions and call them when necessary. This will make your code a thousand times more readable, and your colleagues will thank you for it.
This goes in a similar direction. Don’t do stuff like this:
print("one"); print("two")
It might look sophisticated, but it’s not. When you get an error in a specific line, you’ll want to know exactly what that line does so you can debug it quickly. So split it up; each line does its own thing.
Python is a dynamically typed language. While that’s great to for producing code quickly, it can make debugging harder.
Since Python 3.5, you can prepare for debugging by adding type hinting. That doesn’t affect the code since the interpreter ignores it. But it’s helpful for you because you can instantly see if you’re passing something wrong to a function.
Type hinting works like in the following example:
>>> def sayhello(day:str, name:str) -> str:...     return f"Happy {day}, {name}. Welcome to Python!"
The :str indicates that the arguments of the function are expected to be strings, and the -> str part indicates that the function should return a string.
Don’t settle for second best. The fact that Python is still the indisputable king of general-purpose programming languages is no excuse for writing mediocre code.
You’ll make your code stand out by keeping to style conventions, documenting changes properly, and keeping your code concise. In some cases, that will even improve its performance. In any case, it will make it much easier for you and your colleagues to read and maintain the code.
While this list contains a whole lot of tips and tricks for you, it’s certainly not exhaustive. If I’ve missed anything, let me know in the comments. In the meantime, happy coding!
Edit: As Wink Saville pointed out, the result of getsizeof() varies, depending on the machine, architecture and Python version. That’s included now. Also, Oliver Farren correctly stated that the underscore _ is indeed a variable that takes up memory. The text now includes that using the underscore a convention that adds to readability. And as Michael Frederickson pointed out on Twitter, the dir() function is available from Python 2.7 onwards. The story had mistakenly stated that it’s available from Python 3 only.