Python
General Tips
- Try to follow PEP8, except for line length. Use a linter (like pylint) to enforce them automatically in your code.
- Use full names for variables where possible. For example, choose
job
overj
,something_longer
oversl
, but notthis_variable_name_is_the_longest_name_evar_celery_man
. - Functions and classes should have at least minimal docstrings. A good guide to write them can be found in PEP 0257.
- No mutable default params please! No
def foo(bar={'my': 'default'}):
and nodef foo(bar=MyClass()):
(assuming theMyClass
instance is mutable) - For methods exposed to the outside world, especially exposed methods that read lists of data from the database, default limits should always be applied to prevent accidental or intentional DOS attacks. Such an example can be found in
webapp/controllers/locations.py:get_locations(..)
. Before its latest rewrite, it did not apply any bounds on thecount
argument, causing severe load issues on web application servers.
Syntax
Rules to ease reviews
- Don’t leave trailing white spaces.
- Add a new line char at the end of each file.
- Add trailing commas when enumerating.
from foo import (
bar,
baz, # <--
)
# Or
d = {
"key1": "value",
"key2": 123, # <--
}
Declaring exceptions
The example
class MyAppValueError(ValueError):
def __init__(self, foo, *args):
# Special attribute you desire with your Error,
# perhaps the value that caused the error?:
self.foo = foo
# Allow users initialize misc. arguments as any other builtin Error
super(MyAppValueError, self).__init__(message, foo, *args)
There’s no need to write your own __str__
or __repr__
. The builtin ones
are very nice.
The example, expanded
Name your exceptions in accordance with
PEP8, i.e. use
suffixes like Error
or Warning
where appropriate.
Exception classes you declare should inherit (either directly or indirectly)
from the Exception
class. If there’s more that can be told about the nature
of the exceptional situation, pick a more specific exception class to inherit
from, from the hierarchy of predefined exceptions.
The last parameter of __init__()
method should be *args
to allow for
passing arbitrary number of arguments, which in turn are passed on to
super(...).__init__(..., *args)
. That way, the Liskov Substitution
Principle isn’t violated between your exception class and the Exception
class.
Avoid passing human-readable messages to exception constructors, prefer passing problematic variable values instead and naming the exception class descriptively. Rationale: Exceptions are entities belonging to the realm of the code, not to that of the users. Users aren’t being shown raw exceptions, they’re being shown error messages which are an interpretation of caught exceptions. There are considerations like localization and formatting of the message that need to be taken into account before showing a message to the user. Trying to address such considerations at the point in code at which the exception is risen is simply impractical. Therefore, put all the constituents of a good error message as exception class fields instead, and leave putting together the error message up to the handler of the exception. How the error message will be presented depends on where the code will be used: Web application may need to present it differently than an API.
class MissingContactPhoneNumberError(Exception):
def __init__(self, contact_uuid, *args):
self.contact_uuid = contact_uuid
super(MissingContactPhoneNumberError, self).__init__(self.message, contact_uuid, *args)
and then at the exception handling point in code:
try:
[...]
except MissingContactPhoneNumberError as e:
print "Contact lacks a phone number: {}".format(e.contact_uuid)
Handling exceptions
When writing controllers which handle a lot of Exceptions, try to encapsulate
the logic within one try..except
:
try:
job = get_my_job(job_ref) # Raises some exception when job_ref isn't correct
job.change_it_somehow('weee') # Raises some other exception just because
job.delete('never wanted it anyways') # Raises another exception
except MyJobWasntFoundException as e: # Handle each Exception individually with a descriptive name
log.info(e.message) # Potentially log as error if serious
except JobCantBeAltered as e:
log.info(e.message)
except JobDeleteFailedException as e:
log.info(e.message)
Avoid extracting text from exceptions messages — there’s usually some other way:
try:
[...]
DBSession.flush() # tease out database integrity constraint errors
except IntegrityError as e:
if "name" in e.message:
raise CFSValidationError("DUPLICATE NAME",
"Duplicate name {!r}".format(cfd.name))
It has a potential to fail tests when a message text is changed.
SQL string interpolation
In those occasions where a database query cannot be performed using SQLAlchemy
statements, and you need to run an SQL query using DBSession.execute
, try to
use SQLAlchemy string interpolation.
For table and column names you’ll still need to format the query manually, though.
The following example shows a combination of both approaches:
DBSession.execute(
"DELETE /* rid={rid} */ FROM {table} WHERE {filter}=:scope_val LIMIT :size"
.format(rid=CURRENT.request_id(), table=klass.__tablename__, filter=filter_name), {
"scope_val": scope_val,
"size": chunk_size,
}
)
If the query doesn’t take any values, you can still ensure correctness by
wrapping the query with sqlalchemy.text
:
from sqlalchemy import text
DBSession.execute(
text("""INSERT INTO {table} VALUES(:business_ref, :name, LAST_INSERT_ID(1 + :inc), version) """)
)
As counter example, this is what we should try to avoid in order to prevent SQL Injection issues:
# 5 could have been some external input
result = session.execute(
"SELECT * FROM user WHERE id={}".format(5)
)