Book Review: "Effective Python: 59 Specific Ways to Write Better Python", by Brett Slatkin

This book is definitely geared towards intermediate Python developers, or those with a basic understanding of Python. As this book is primarily a list of Python tips, I will go through the points that I have personally encountered, and if applicable any improvements I have on the author's comments.

1. Know Which Version of Python You're Using

I have been using Python 2.7 at work; the latest Python standard is 3.6. There are considerable breaking changes between Python 2 and Python 3 and new projects should all be using Python 3. If you are using Python 2, definitely refactor as you develop to keep with Python 3 syntax, keeping in mind that Python 2 maintenance will cease in 2020.

2. Follow the PE8 Style Guide

Definitely use PEP8 as a reference, but do not feel bound to strictly follow every single rule. Some rules, such as hard-wrapping 80 characters per line, may be outdated. Instead, use pylint and a .pylintrc file in order to maintain code consistency.

3. Know the Differences between bytes, str, and unicode

One major reason Python 3 took such a long time to adopt was because it used unicode strings by default, while Python 2 used ascii. So if you used strings in your packages in Python 2, porting your packages to Python 3 took nonzero effort. Definitely keep this in mind if you are still on Python 2.

7. Use List Comprehensions Instead of map and filter

At the beginning of this year, I didn't know what list comprehensions are. Now that I've used them for a good amount of time, I wholeheartedly recommend them. It's clear, it's functional, it's Pythonic. Even nested list comprehensions can make sense, given the right circumstances. Definitely worth learning.

10: Prefer enumerate over range

Why wouldn't you want access to the element and the index in the same loop? If you don't need something, just use _ to zero it out.

14. Prefer Exceptions to Returning None

If something bad has happened, you want your code to break with an exception. That way, you have a full stack trace, args, everything you need in order to debug and fix the code. If you return None, you're just passing the error along until it breaks something somewhere along the critical path. Data issues are no excuse; data should be sanitized and validated before being run through business logic.

18. Reduce Visual Noise with Variable Positional Arguments (*args)

In my experience, I'm not a big fan of using *args because I like my arguments strictly defined. If a method has too many arguments (more than 7), I would rather break up the method if possible. Under certain circumstances, say defining a public API that cannot change input parameters easily, I would be more willing to consider *args.

19. Provide Optional Behavior with Keyword Arguments

I really don't like **kwargs, and differing number of arguments within a given function. For me, function definitions change too much for business reasons in order to use keyword arguments effectively.

20. Use None and Docstrings to Specify Dynamic Default Arguments

OMG YES. I didn't fully understand this until I was burned by it twice, but this is extremely important. NEVER put mutable objects as default params; always assign within a method. This is because a mutable object is instantiated when the function is defined, not when it is called. It is effectively a global variable. This is very bad. Define a default argument of None, and if the value is None within the method, instantiate the object. Document as needed.

22. Prefer Helper Classes Over Bookkeeping with Dictionaries and Tuples

Python is an object-oriented language; classes have much more power over more basic object types. However, when you should use classes over dicts and tuples is a discretionary matter that should be left to the developer / team to decide.

27. Prefer Public Attributes Over Private Ones

I was going to make a joke about how there is no privacy anymore, but in Python's case it isn't really a joke. If you want to mark something as "you shouldn't access this", use protected field syntax, such as _protected_field vs public_field.

29. Use Plain Attributes Instead of Get and Set Methods

I've seen getters and setters in Python code; it's like the Maginot Line (and about as effective too). Somebody will just go around it.

42. Define Function Decorators with functools.wraps

Very useful for adding in separate functionality. I've used it to protect functions at the API level (adding in token_required for authentication to business logic, for example); it's very good.

44. Use datetime Instead of time for Local Clocks

Completely agree with the author in this case; there is no reason to use the time module. Use datetime and pytz. And for God's sakes, ensure that all timestamps have a timezone. I've seen timestamps in Eastern, UTC, and no timezone at all in the same code; it is very confusing to sort through those. I'm not the only one: https://xkcd.com/1883/

49. Write Docstrings for Every Function, Class, and Module

YES YES YES. I said earlier you don't need to follow every rule of PEP8; you really should follow this one. Without good documentation, your code is just yours and nobody else's, and that makes collaboration and cooperation very difficult.

50. Use Packages to Organize Modules and Provide Stable APIs

I really like Python's packages. All you need to do is stick an __init__.py within the directory you want to make a module, and Python will automatically index it for you. That being said, there are other ways in order to create stable APIs. I highly recommend conda and conda build; you can add your own channels to deploy to a private server (unlike pip, which can only deploy to PyPi) and all dependencies are included and tests are run during build as specified in your meta.yaml file. Definitely consider if your module code gets out of hand.

53. Use Virtual Environments for Isolated and Reproducible Dependencies

I highly recommend this approach. I really wish pip or conda was more like mix for Elixir or yarn for JavaScript, where they have a lock file that updates whenever you install a package. This is important because you can pip install a package and your requirements.txt file will not be updated. Having a virtual environment means that's just moot. If you know you have the latest dependencies, you can just do pip freeze > requirements.txt and push up to remote for others to use. If you installed on system, well...

54. Consider Module-Scoped Code to Configure Deployment Environments

I do not recommend this approach; I'm much more a fan of using .env or similar environment variable configuration, and having Python read those into Python variables. This is because configuration info can be highly sensitive (keys and secrets for API services, for example) and you do not want to check those into version control, because you will never be able to get them back out (unless you did a git reset, which is painful).

56. Test Everything with unittest

I do not recommend this approach. pytest offers so much more out of the box than unittest does. This might be different for Python 3. I do agree that tests should be inside modules (I don't think PyTest has that constraint).

57. Consider Interactive Debugging with pdb

A preference choice, but check out ipdb; it's a lot prettier in the terminal, and useful pretty too.

58. Profile Before Optimizing

Definitely agree with the message. One thing to make sure is that you are calling the functions you think you are calling. Sometimes you may be calling a fast function multiple times. A call graph can be helpful here; use a tool like pyprof2calltree to generate call graphs from .prof files.

There's a lot of knowledge in this book, and a lot of stuff that I haven't used, understood, or encountered in my day-to-day work. Definitely worth a buy.