Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/svisser/python-improvements

Common improvements for your Python projects
https://github.com/svisser/python-improvements

Last synced: 5 days ago
JSON representation

Common improvements for your Python projects

Awesome Lists containing this project

README

        

Improvements for Python projects
================================

This is a list of common improvements for your Python code.

These are not necessarily mistakes but they can improve the quality of your code.

**Use sys.version_info instead of sys.version**:
The string `sys.version` is sometimes used to determine version of the
Python interpreter (such as `sys.version[0] > "2"` to run code only for Python 3+).
However, this is a string and there is no guarantee across Python implementations
that the Python version is at the beginning of the string. Instead you should
use the tuple `sys.version_info`. For example: `sys.version_info[0] > 3`. As of
Python 2.7 you can also access the numbers using names, such as `sys.version_info.major`
for the major Python version (i.e., `sys.version_info[0]` is the same as `sys.version_info.major`).

**Using sys.version for platform checks**:
Similarly, the string `sys.version` also should not be used for determining
the name of the platform (e.g., `'PyPy' in sys.verion`). Python has a module
called `platform` that should be used to learn the interpreter (CPython, PyPy, ...)
or the operating system that the code is running on.

**Ignoring potential future Python versions**:
As part of porting a codebase from Python 2 to a Python 2/3 compatible codebase
some people forget that there most likely will be a Python 4. This means checking
for `if sys.version_info[0] == 3` works for now but will fail to work in Python 4.
The code that you're executing for Python 3 would need to be executed for
Python 4 as well (as Python 4 won't break backwards compatibility with 3). The
Unicode / bytes separation still applies and therefore the code would need to
be `if sys.version_info[0] >= 3: ... else: ...`. If Python 4 does introduce
any specific changes then things can be changed at that time - but without
further changes the Python 3 code would apply.

**Opening files without closing them**:
When you `open()` a file you also need to ensure the file is closed by calling
`close()` on the file object. This means system resources are released and
that any remaining writes are indeed written to the file. For example:

f = open(...)
# use f here
f.close()

In Python 2.6+ you can open a file by using the with statement:

with open(...) as f:
# use f here

This will call `f.close()` automatically as soon as execution leaves the with block.
In many cases this approach is preferred over calling `f.close()` yourself as
this also ensures the file is closed when exceptions occur.

**Ensure file handles are closed in setup.py**:
In `setup.py` you may wish to read the contents of other files, such as
the README or CHANGELOG, and pass them to `setup()` without explicitly closing
the file yourself. Although running the `setup.py` file will be a short-lived
process, best practises should apply here as well so it's better to close
the file yourself:

with open(...) as f:
readme = f.read()

**Using type() instead of isinstance()**:
If you're using `type(v)` to check the type of a variable, you may not be
including all the types that you wish to check for. For example, if you write
`if type(d) is dict` then you're only checking that `d` is a dictionary object
but not whether `d` is a subclass of `dict`. In this case it's better to use
`if isinstance(d, dict)` to check whether `d` is a dictionary or a subclass
of a dictionary.

But even this may be too restrictive: you may wish to check
whether a "dictionary-like" object was provided, not necessarily a dictionary
or subclass of `dict`. Python provides various so-called "abstract base classes"
that someone can use as base class for their classes. For example, the abstract
base class `collections.Mapping` (from the `collections` module) is for objects
from which you can "get a value for a given key", that you can
iterate over it and that have a length (size).

If an object that provides this functionality is "good
enough" for your code then you don't need to check that `type(d) is dict` or
`if isinstance(d, dict)`, you can broaden the check to be
`if isinstance(d, collections.Mapping)`. This if statement will be true
for dictionaries, subclasses of dictionaries as well as objects such as
`collections.Counter`, `collections.OrderedDict` and `collections.defaultdict`
from Python's standard library.

**Not using 'Easier to ask for forgiveness than permission'**:
Even with the above use of `isinstance` and other type checking it's possible that
someone else wishes to use your code with objects that they have developed.
These may not inherit from the base class that you're checking for and, as such,
it can be better to simply assume that certain methods or attributes will be available:

try:
result = obj.foobar()
except AttributeError:
result = None

This allows anyone reading your code to construct an `obj` that has the method `foobar` and
return the desired value for `result`. If this method is not available then an `AttributeError`
will be raised (which the rest of your code needs to adapt to).
In Python this approach of assuming things and catching an exception in case it didn't work
out is called 'Easier to ask for forgiveness than permission' (EAFP). This coding style can make
your code easier to read and more flexible in case you wish to pass in another object.

At the other end we have the 'Look before you leap' (LBYP) approach which would use various checks to
prevent bad things from happening (i.e., only allow objects for which you know it'll work to
pass through). This is good at times but it can also be too restrictive when only a limited number
of objects are accepted / considered valid.

**Decorating a function without preserving details**:
If you've used a decorator to change the behaviour of a function, it's possible
that you're not preserving the details of the original function, such as its
name or its docstring. For example:

def f():
"""Documentation here"""
return 3

You can now access the function name by looking at `f.__name__` and you can
access the docstring by looking at `f.__doc__`. If you're now applying a decorator
that returns a new function but without copying over these attributes, you won't
be able to access these values. For example:

@my_decorator
def f():
"""Documentation"
return 3

Even though the function is decorated, the values of `f.__name__` and `f.__doc__`
should still be the same as before. Whether or not this is the case depends on
the implementation of `my_decorator`. So a decorator that you've written needs
to preserve these details (most often by returning a new function that has the
same values). You can do this by using `functools.wraps` which
allows you to indicate that one function wraps the other function and that
the relevant details should be preserved.

This also matches the intent of a decorator: a decorator is aimed
at modifying the behaviour of the function but it's still the "same" function
for most intents and purposes (i.e., with the same name and docstring). If you're
using a decorator to pass default values to the function, it may be better to
use `functools.partial` which doesn't have the same expectation to return
the "same" function.