Readability counts.
Ignacy SokoĊowski
Ignacy SokoĊowski
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Why readability counts?
Who reads your code?
Style Guide for Python code.
A reader knows what to expect, they can guess the object type by its name.
Class name should be CamelCase.
BAD:
class request(object):
pass
response = request() # What the user expects.
request = request() # What the author meant.
GOOD:
class Request(object):
pass
request = Request()
Function or method name should be lowercase.
BAD:
def Request(url):
pass
request = Request('http://...') # What the user expects.
response = Request('http://...') # What the author meant.
GOOD:
def request(url):
pass
response = request('http://...')
Module name should be lowercase.
BAD:
import os.Path
path = os.Path('/tmp/foo.json') # What the user expects.
os.Path.exists('/tmp/foo.json') # What the author meant.
GOOD:
import os.path
os.path.exists('/tmp/foo.json')
Constants should be UPPERCASE.
BAD:
from django.conf.settings import TimeZone
time_zone = TimeZone('UTC') # What the user expects.
>>> TimeZone # What the author meant.
'UTC'
GOOD:
from django.conf.settings import TIME_ZONE
>>> TIME_ZONE
'UTC'
Function and method name should start with a verb, suggestive on the action.
BAD:
class JSONFile(object):
def __init__(self, path):
self.path = path
def content(self):
with open(self.path) as f:
raw_content = f.read()
content = json.loads(raw_content)
return content
json_file = JSONFile('./data.json')
# User expects 'content` to be an attribute.
if json_file.content:
title = json_file.content['title']
body = json_file.content['body']
GOOD:
class JSONFile(object):
def __init__(self, path):
self.path = path
def read(self):
with open(self.path) as f:
raw_content = f.read()
content = json.loads(raw_content)
return content
json_file = JSONFile('./data.json')
content = json_file.read()
if content:
title = content['title']
body = content['body']
Booleans should start with is_, has_, etc.
BAD:
class User(object):
def __init__(self, username, password, avatar):
self.username = username
self.password = password
self.avatar = avatar
def get_avatar_path(self):
if self.avatar:
return os.path.join('avatars', self.username + '.png')
>>> user = User('admin', 'passwd', 'me.png') # What the user expects.
>>> user.avatar
'me.png'
>>> user.get_avatar_path()
'avatars/me.png'
Booleans should start with is_, has_, etc.
BAD:
class User(object):
def __init__(self, username, password, avatar):
self.username = username
self.password = password
self.avatar = avatar
def get_avatar_path(self):
if self.avatar:
return os.path.join('avatars', self.username + '.png')
>>> user = User('admin', 'passwd', avatar=True) # What the author meant.
>>> user.avatar
True
>>> user.get_avatar_path()
'avatars/admin.png'
>>> user = User('admin', 'passwd', avatar=False) # What the author meant.
>>> user.avatar_path
None
Booleans should start with is_, has_, etc.
GOOD:
class User(object):
def __init__(self, username, password, has_avatar):
self.username = username
self.password = password
self.has_avatar = has_avatar
def get_avatar_path(self):
if self.has_avatar:
return os.path.join('avatars', self.username + '.png')
>>> user = User('admin', 'passwd', has_avatar=True)
>>> user.has_avatar
True
>>> user.get_avatar_path()
'avatars/admin.png'
>>> user = User('admin', 'passwd', has_avatar=False)
>>> user.get_avatar_path()
None
Names should be descriptive but not too long.
BAD:
class ArticleTitleCanNotBeLongerThan100CharactersError(Exception):
pass
from project.blog.exceptions import ArticleTitleCanNotBeLongerThan100CharactersError
GOOD:
class TitleLengthError(Exception):
"""Raised when article title is longer than 100 characters."""
from project.blog.exceptions import TitleLengthError
raise TitleLengthError("Title longer than 100 characters: %s", title)
Space after comma, full stop, colon, etc.
foo(1,2) # Bad.
foo(1.2)
foo(1, 2)
foo(one, two,three, four) # Bad.
foo(one, two.three, four)
foo(one, two, three, four)
[1,2,3,4,5,6,7] # Bad.
[1,2,3,4.5,6,7] # Bad.
[1, 2, 3, 4, 5, 6, 7]
[1, 2, 3, 4.5, 6, 7]
Indentation should be always a multiple of four.
BAD:
some_dictionary = {'ok': 200, 'not_found': 404, 'forbidden': 403,
'bad_request': 400, 'internal_server_error': 500}
@@ -27,7 +27,6 @@
- some_dictionary = {'ok': 200, 'not_found': 404, 'forbidden': 403,
- 'bad_request': 400, 'internal_server_error': 500}
+ status_codes = {'ok': 200, 'not_found': 404, 'forbidden': 403,
+ 'bad_request': 400, 'internal_server_error': 500}
BAD:
some_dictionary = {'ok': 200, 'not_found': 404, 'forbidden': 403,
'bad_request': 400, 'internal_server_error': 500}
@@ -27,7 +27,6 @@
- some_dictionary = {'ok': 200, 'not_found': 404, 'forbidden': 403,
- 'bad_request': 400, 'internal_server_error': 500}
+ some_dictionary = {'ok': 200, 'unauthorized': 401, 'not_found': 404,
+ 'forbidden': 403, 'bad_request': 400,
+ 'internal_server_error': 500}
BETTER:
some_dictionary = {
'ok': 200,
'not_found': 404,
'forbidden': 403,
'bad_request': 400,
'internal_server_error': 500}
@@ -27,7 +27,6 @@
- some_dictionary = {
+ status_codes = {
'ok': 200,
BETTER:
some_dictionary = {
'ok': 200,
'not_found': 404,
'forbidden': 403,
'bad_request': 400,
'internal_server_error': 500}
@@ -27,7 +27,6 @@ some_dictionary = {
'ok': 200,
+ 'unauthorized': 401,
'not_found': 404,
BETTER:
some_dictionary = {
'ok': 200,
'not_found': 404,
'forbidden': 403,
'bad_request': 400,
'internal_server_error': 500}
But here's a problem:
@@ -27,7 +27,6 @@ some_dictionary = {
'bad_request': 400,
- 'internal_server_error': 500}
+ 'internal_server_error': 500,
+ 'conflict': 409}
BEST:
some_dictionary = {
'ok': 200,
'not_found': 404,
'forbidden': 403,
'bad_request': 400,
'internal_server_error': 500,
}
@@ -27,7 +27,6 @@ some_dictionary = {
'internal_server_error': 500,
+ 'conflict': 409,
}
BAD:
some_long_name = SomeLongClass.some_long_method_name(some_dictionary['foo'],
some_dictionary['bar'],
some_dictionary['baz'],
some_dictionary['foobar'])
BETTER:
some_long_name = SomeLongClass.some_long_method_name(
some_dictionary['foo'], some_dictionary['bar'], some_dictionary['baz'],
some_dictionary['foobar'])
BEST:
some_long_name = SomeLongClass.some_long_method_name(
some_dictionary['foo'],
some_dictionary['bar'],
some_dictionary['baz'],
some_dictionary['foobar'])
BAD:
if (self.foo(some_argument) and
self.bar(another_argument) and
self.baz(third_argument) and
self.spam(fourth_argument)):
self.foobar(some_argument)
GOOD:
if (self.foo(some_argument) and
self.bar(another_argument) and
self.baz(third_argument) and
self.spam(fourth_argument)):
self.foobar(some_argument)
BAD:
from django.shortcuts import render_to_response
import datetime
import requests # Is requests in the standard library?
from django.views.generic.list_detail import object_detail
from .utils import something
import os
GOOD:
import datetime
import os
import requests
import sqlalchemy
from . import utils
from . import exceptions
You know what 3rd party libraries are used.
It's easier to find a specific import.
import argparse
import calendar
import datetime
import logging
import io
import os
import re
import subprocess
import time
import threading
BAD:
from requests import *
from sqlalchemy.orm import *
# Where do these come from?
session = Session()
get(foo)
GOOD:
import requests
import sqlalchemy.orm
session = sqlalchemy.orm.Session()
requests.get(foo)
It's often a better idea to import module instead of each name directly.
BAD:
from requests import get, post, Session, session, HTTPError, Response
# Somewhere at the bottom of a long module:
session = Session()
get(foo)
from requests import request
def some_django_view(request): # Overriding 'request'.
response = request('POST', 'http://...')
It's often a better idea to import module instead of each name directly.
GOOD:
import requests
# Somewhere at the bottom of a long module:
session = requests.Session()
requests.get(foo)
import requests
def some_django_view(request):
response = requests.request('POST', 'http://...')
import pylons
def do_something_using_pylons_config(config=None):
config = config or pylons.config
[
{"title": "foo bar", "body": "Lorem ipsum ..."},
{"title": "bar foo", "body": "Dolor sit amet ..."}
]
BAD:
def get_first_initial_bad(file_path):
return json.loads(open(file_path).read())[0]['title'][0].upper()
[
{"title": "foo bar", "body": "Lorem ipsum ..."},
{"title": "bar foo", "body": "Dolor sit amet ..."}
]
GOOD:
def get_first_initial_good(file_path):
with open(file_path) as f:
json_content = f.read()
articles = json.loads(json_content)
first_article = articles[0]
title = first_article['title']
first_letter = title[0]
initial = first_letter.upper()
return initial
[]
BAD:
Traceback (most recent call last):
File "./initial.py", line 7, in get_first_initial_bad
return json.loads(open(file_path).read())[0]['title'][0].upper()
IndexError: list index out of range
GOOD:
Traceback (most recent call last):
File "./initial.py", line 14, in get_first_initial_good
first_article = articles[0]
IndexError: list index out of range
[{"title": []}]
BAD:
Traceback (most recent call last):
File "./initial.py", line 7, in get_first_initial_bad
return json.loads(open(file_path).read())[0]['title'][0].upper()
IndexError: list index out of range
GOOD:
Traceback (most recent call last):
File "./initial.py", line 16, in get_first_initial_good
first_letter = title[0]
IndexError: list index out of range
[{"title": null}]
BAD:
Traceback (most recent call last):
File "./initial.py", line 7, in get_first_initial_bad
return json.loads(open(file_path).read())[0]['title'][0].upper()
TypeError: 'NoneType' object has no attribute '__getitem__'
GOOD:
Traceback (most recent call last):
File "./initial.py", line 16, in get_first_initial_good
first_letter = title[0]
TypeError: 'NoneType' object has no attribute '__getitem__'
null
BAD:
Traceback (most recent call last):
File "./initial.py", line 7, in get_first_initial_bad
return json.loads(open(file_path).read())[0]['title'][0].upper()
TypeError: 'NoneType' object has no attribute '__getitem__'
GOOD:
Traceback (most recent call last):
File "./initial.py", line 14, in get_first_initial_good
first_article = articles[0]
TypeError: 'NoneType' object has no attribute '__getitem__'
{"title": "foo"}
BAD:
Traceback (most recent call last):
File "./initial.py", line 7, in get_first_initial_bad
return json.loads(open(file_path).read())[0]['title'][0].upper()
KeyError: 0
GOOD:
Traceback (most recent call last):
File "./initial.py", line 14, in get_first_initial_good
first_article = articles[0]
KeyError: 0
[{"title": {}}]
BAD:
Traceback (most recent call last):
File "./initial.py", line 7, in get_first_initial_bad
return json.loads(open(file_path).read())[0]['title'][0].upper()
KeyError: 0
GOOD:
Traceback (most recent call last):
File "./initial.py", line 16, in get_first_initial_good
first_letter = title[0]
KeyError: 0
[{"title": ""}]
BAD:
Traceback (most recent call last):
File "./initial.py", line 7, in get_first_initial_bad
return json.loads(open(file_path).read())[0]['title'][0].upper()
IndexError: string index out of range
GOOD:
Traceback (most recent call last):
File "./initial.py", line 16, in get_first_initial_good
first_letter = title[0]
IndexError: string index out of range
BAD:
def find_user(username):
try:
user = Users.get(username)
except:
user = None
return user
>>> find_user('foo')
<User foo>
>>> find_user('bar')
None
>>> find_user(1)
None
def find_user(username):
try:
user = Users.get(username)
except UserLookupError:
user = None
return user
>>> find_user('foo')
<User foo>
>>> find_user('bar')
None
>>> find_user(1)
Traceback (most recent call last):
File "api.py", line 7, in get
username = username.lower()
AttributeError: 'int' object has no attribute 'lower'
BAD:
try:
user = find_user(email)
message = EmailMessage(from=user.email, subject='Foo', body='Bar')
message.send(recipient_email)
except EmailError:
self.log_failure(email)
return {'user_email': email, 'recipient_email': recipient_email}
GOOD:
try:
user = find_user(email)
except EmailError:
self.log_failure(email)
else:
message = EmailMessage(from=user.email, subject='Foo', body='Bar')
message.send(recipient_email)
return {'user_email': email, 'recipient_email': recipient_email}
Avoid long functions and methods.
Split them into many small ones.
Refactor functions into classes if they share the same data via arguments.
It's easier to unittest small functions.
Classes can be subclassed to extend their methods. Functions can't.
Start Writing More Classes by Armin Ronacher http://lucumr.pocoo.org/2013/2/13/moar-classes/
class XMLParser(object):
def validate_signature(self, xml):
xml_sig = xml.xpath('./Signature/text()')
timestamp = xml.xpath('./Timestamp/text()')
# Calculate valid signature.
sig = ''.join([timestamp, self.developer, self.app, self.certificate])
md5sig = hashlib.md5(sig.encode()).digest()
b64sig = base64.b64encode(md5sig).decode()
if xml_sig != b64sig:
raise SignatureError('XML signature is invalid.')
class XMLParser(object):
def validate_signature(self, xml):
xml_sig = self._find_signature(xml)
timestamp = self._find_timestamp(xml)
valid_sig = self._calculate_signature(timestamp)
if xml_sig != valid_sig:
raise SignatureError('XML signature is invalid.')
def _find_signature(self, xml):
return xml.xpath('./Signature/text()')
def _find_timestamp(self, xml):
return xml.xpath('./Timestamp/text()')
def _calculate_signature(self, timestamp):
sig = ''.join([timestamp, self.developer, self.app, self.certificate])
md5sig = hashlib.md5(sig.encode()).digest()
b64sig = base64.b64encode(md5sig).decode()
return b64sig
You read from top to bottom.
class XMLParser(object):
def _find_timestamp(self, xml):
return xml.xpath('./Timestamp/text()')
def _find_signature(self, xml):
return xml.xpath('./Signature/text()')
@classmethod
def from_config(cls, config):
return cls(config['developer'], config['app'], config['certificate'])
def _calculate_signature(self, timestamp):
sig = ''.join([timestamp, self.developer, self.app, self.certificate])
md5sig = hashlib.md5(sig.encode()).digest()
b64sig = base64.b64encode(md5sig).decode()
return b64sig
def __init__(self, developer, app, certificate):
self.developer = developer
self.app = app
self.certificate = certificate
def validate_signature(self, xml):
xml_sig = self._find_signature(xml)
timestamp = self._find_timestamp(xml)
valid_sig = self._calculate_signature(timestamp)
if xml_sig != valid_sig:
raise SignatureError('XML signature is invalid.')
class XMLParser(object):
def __init__(self, developer, app, certificate):
self.developer = developer
self.app = app
self.certificate = certificate
@classmethod
def from_config(cls, config):
return cls(config['developer'], config['app'], config['certificate'])
def validate_signature(self, xml):
xml_sig = self._find_signature(xml)
timestamp = self._find_timestamp(xml)
valid_sig = self._calculate_signature(timestamp)
if xml_sig != valid_sig:
raise SignatureError('XML signature is invalid.')
def _find_signature(self, xml):
return xml.xpath('./Signature/text()')
def _find_timestamp(self, xml):
return xml.xpath('./Timestamp/text()')
def _calculate_signature(self, timestamp):
sig = ''.join([timestamp, self.developer, self.app, self.certificate])
md5sig = hashlib.md5(sig.encode()).digest()
b64sig = base64.b64encode(md5sig).decode()
return b64sig