Practical Clean Code
Israel Saeta Pérez (@dukebody)


projects delay/failure
sneaky bugs
developer suffering & quit
contagious mess
What is clean code?
- reads like prose
- elegant and efficient
- makes it hard for bugs to hide
- can be enhanced by a developer other than its original author
- provides one obvious way to make things
let's start easy
Use a style guide checker
following convention makes it easier to read
- pep8
- flake8 - integrate with git hooks and CI
- editor auto-format

Use meaningful variable and function names
# bad
ret = []
for l in legs:
s = l.get_first_segment()
ret.append(s)
return ret
# better
leg = Leg()
segment = leg.get_first_segment()
first_segments = []
for leg in legs:
segment = leg.get_first_segment()
first_segments.append(s)
return first_segments
We have autocomplete!
# unnecessary verbosity
class Address:
def __init__(self, address_street, address_country):
self.address_street = address_street
self.address_country = address_country
# better
class Address:
def __init__(self, street, city):
self.street = street
self.city = city
Use context and namespacing
# how do I know without looking at the class definition?
res = BookingResult(
booking.id,
booking.type,
booking.amount,
booking.currency,
travelers_count,
user.currency_preference,
True,
booking_request.validated_at,
None
)
# better
booking_result = BookingResult(
id=booking,
type=booking.type,
amount=booking.amount,
currency=booking.currency,
travelers_count=travelers_count,
currency_user=user.currency_preference,
error=True,
validated_at=booking.validated_at,
price_change=None
)
Use kwargs in functions with many arguments
# OK
if action in ['create', 'update', 'delete']:
check_permission(user, 'write')
# better
is_write_action = action in ['create', 'update', 'delete']
if is_write_action:
check_permission(user, 'write')
# OK?
if self.from_subject.subject_company != self.to_subject.subject_company:
raise InvalidRelation()
# better
is_same_company = from_subject.subject_company != to_subject.subject_company
if not is_same_company:
raise InvalidRelation()
# even better?
if not is_same_company(from_subject, to_subject):
raise InvalidRelation()
clarify conditionals
functions should only do
one obvious thing
# bad
def get_digest_airlines(offer):
digest = ""
items = offer.items
offer_airlines = set()
for item in items:
item_digest, item_airlines = get_item_digest(item)
digest += item_digest
offer_airlines = offer_airlines.union(item_airlines)
return digest, offer_airlines
# good
def get_digest(self, offer):
digest = ""
items = offer.items
for item in items:
item_digest, item_airlines = self.get_item_digest(item)
digest += item_digest
return digest
def get_airlines(offer):
items = offer.items
airlines = set()
for item in items:
item_airlines =.get_item_airlines(item)
airlines = airlines.union(item_airlines)
return airlines
avoid returning non-homogeneous tuples
break long functions
into smaller ones
make it read like prose, cascade
# bad
def get_duplicated_trips(trip):
# calculate signature for trip
signature = ''
for leg in trip.legs:
for segment in leg.segments:
signature += get_signature(segment)
# get trips candidate for being duplicate
company = trip.company
candidate_trips = Trip.objects.filter(company=company)
# calculate signature for each trip
candidate_signatures = []
for candidate_trip in candidate_trips:
candidate_signature = ''
for leg in candidate_trip.legs:
for segment in leg.segments:
candidate_signature += get_signature(segment)
candidate_signatures.append(candidate_signature)
# get the trips for which the signature matches
duplicated_trips = []
for candidate_trip, candidate_signature in zip(candidate_trips, candidate_signatures):
if candidate_signature == signature
duplicated_trips.append(candidate_trip)
Get duplicated trips
# better
def get_duplicated_trips(trip):
signature = get_trip_signature(trip)
candidate_trips = get_candidate_trips(trip)
return [trip for trip in candidate_trips if get_trip_signature(trip) == signature]
def get_trip_signature(trip):
signature = ''
for leg in trip.legs:
for segment in leg.segments:
signature += get_signature(segment)
return signature
def get_candidate_trips(trip):
company = trip.company
return Trip.objects.filter(company=company)
Get duplicated trips
# bad
def get_total_invoice_amounts(year, month):
# get inovices for given month
start = datetime(year, month, 1)
next_month = month + 1 if month != 12 else 1
end = datetime(year, next_month, 1)
invoices = Invoice.objects.filter(created_at__gte=start, created_at__lte=end)
# sum amounts for invoices
total = 0
for invoice in not invoices:
for item in invoice.items:
total += item.amount
return total
Compute total invoice amounts for month
# better
def get_total_invoice_amounts(year, month):
invoices = get_invoices_for_month(year, month)
return sum(get_amount(invoice) for invoice in invoices)
def get_invoices_for_month(year, month):
start = datetime(year, month, 1)
next_month = month + 1 if month != 12 else 1
end = datetime(year, next_month, 1)
return Invoice.objects.filter(created_at__gte=start, created_at__lte=end)
def get_amount(invoice):
return sum(item.amount for item in invoice)
Compute total invoice amounts for month
Keep cognitive/cyclomatic complexity small
conditionals, for's, nested code...
# bad - cyclomatic complexity of 6
def post_comment(self):
if self.type == 'success':
comment = 'Build succeeded'
elif self.type == 'warning':
comment = 'Build had issues'
elif self.type == 'failed':
comment = 'Build failed'
else:
comment = 'Unknown status'
if self.type == 'success':
self.post(comment, type='success')
else:
self.post(comment, type='error')
# better - both functions have complexity of 1
def get_comment(self):
comments = {
'success': 'Build succeeded',
'warning': 'Build had issues',
'failed': 'Build failed'
}
return comments.get(self.type, 'Unknown status')
def post_comment(self):
comment = self.get_comment(self)
self.post(comment, type=self.type)
principle of least knowledge
(information hiding, encapsulation)
hide the internal madness of your objects
- Rule of "only one dot"
- Create business methods for classes, avoid accessing attributes directly
- Create more classes to hold data and logic to transfer defensiveness from the caller to the callee
# bad
def get_total_invoice_amounts(invoices):
# sum amounts for invoices
total = 0
for invoice in invoices:
# need to know that invoices have items!
for item in invoice.items:
total += item.amount
return total
# better
def get_total_invoice_amounts(invoices):
total = 0
for invoice in invoices:
total += invoice.amount
return total
class Invoice:
@property
def amount(self):
sum(item.amount for item in self.items)
Sum invoices amounts (rule one dot)
# bad
# caller needs to know internals
for leg in dog.legs:
leg.move(forward=10)
# better
dog.move(forward=10)
class Dog:
def move(self, forward):
for leg in self.legs:
leg.move(forward=10)
ask dog move forward (rule one dot)

# bad
def pay(product, wallet):
# also assumes internal structure
wallet.money -= product.price
# better
def pay(product, wallet):
wallet.take(product.price)
class Wallet:
def take(self, price):
if price > self.money:
raise ValueError('Not enough money')
# ...
avoid impossible states
pay(amount, currency):
price = {
'amount': 13,
'currency': 'EUR'
}
pay(price)
class Price:
def __init__(self, amount, currency):
if self.amount < 0:
raise ValueError('Amount cannot be smaller than zero')
self.amount = amount
is self.currency not in ALLOWED_CURRENCIES:
raise ValueError('Unknown currency %s' % currency)
self.currency = currency
mandatory structure, avoid impossible states
# Allows a flight with no legs, or leg without segments
flight = {
'legs': [
{
'key': 'abc',
'segments': [...]
}
]
}
def get_direction(leg):
if len(leg['segments']): # defensive caller
...
else:
# what???
class Leg:
def __init__(self, segments):
assert len(segments) > 0 # defensive callee
self.segments = segments
def get_direction(self):
# we are sure the leg has segment
Premature optimization
is the root of all evil

avoid
premature FLEXIBILITY
because YAGNI, KISS!

if the design is simple, it will be easy to add the features later if needed
continuous refactoring
boy scout rule

avoid superfluous function arguments
# get n posts, sorted by creation date
def get_posts(count, order_reverse=True):
if order_reverse:
order_by = '-created'
else:
order_by = 'created'
return Post.objects.order_by(order_by)[:count]
# perhaps we don't need to control the ordering direction yet?
def get_posts(count):
return Post.objects.order_by('-created')[:count]
avoid unneccessary abstraction
class TemplateEngine:
def render(self, file_path, context):
# ...
class Jinja2TemplateEngine(TemplateEngine):
def render(...):
template = jinja2_env.get_template('my_template.html')
return template.render(**context)
def my_view(...):
template_engine = config.get_template_engine()
body = template_engine.render('my_template.html', context)
return Response(body)
# what if we are only using Jinja2 for now?
def my_view(...):
template = jinja2_env.get_template('my_template.html')
body = template.render(**context)
return Response(body)
focus on making your code easy to follow
optimize based of performance analysis when necessary
# Early loop break
has_luggage = False
for leg in legs:
if has_luggage(leg):
has_luggage = True
break
# Easier to read? If has_luggage is not expensive...
has_luggage = any([has_luggage(leg) for leg in legs])
# Turn into early break without sacrifycing readability
has_luggage = any(has_luggage(leg) for leg in legs)
(line profiler)
# script_to_profile.py
@profile
def maybe_slow_function(numbers):
...
$ kernprof -l script_to_profile.py
# script runs....
$ python -m line_profiler script_to_profile.py.lprof
# output
Timer unit: 1e-06 s
Total time: 0.000649 s
File: <ipython-input-2-2e060b054fea>
Function: maybe_slow_function at line 4
Line # Hits Time Per Hit % Time Line Contents
==============================================================
4 def maybe_slow_function(numbers):
5 1 10 10.0 1.5 total = sum(numbers)
6 1 186 186.0 28.7 divided = [numbers[i]/43 for i in range(len(numbers))]
7 1 453 453.0 69.8 return ['hello' + str(numbers[i]) for i in range(len(numbers))]
tests are also code
use similar (relaxed) principles



cost/effectiveness of unit tests
resources
- Robert C. Martin books (Clean Code, Clean Architecture)
- Learn from open source: AOSA books
thanks!
Practical Clean Code - Python
By Israel Saeta Pérez
Practical Clean Code - Python
- 2,545