2.1. Software Engineering Conventions

2.1.1. PEP 8 - Style Guide for Python Code

2.1.2. Tabs or spaces?

  • 4 spacje

  • IDE zamienia tab na 4 spacje

2.1.3. Line length

  • najbardziej kontrowersyjna klauzula

  • 79 znaków kod

  • 72 znaki docstrings/comments

  • Python standard library is conservative and requires limiting lines to 79 characters (and docstrings/comments to 72)

  • soft wrap

  • co z monitorami 4k?

  • Preferred way of wrapping long lines is by using Python's implied line continuation inside parentheses, brackets and braces.

class FoodProduct(models.Model):
    vitamins_folic_acid = models.DecimalField(verbose_name=_('Folic Acid'), help_text=_('µg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_a = models.DecimalField(verbose_name=_('Vitamin A'), help_text=_('µg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_b1 = models.DecimalField(verbose_name=_('Vitamin B1'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_b2 = models.DecimalField(verbose_name=_('Vitamin B2'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_b6 = models.DecimalField(verbose_name=_('Vitamin B6'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_b12 = models.DecimalField(verbose_name=_('Vitamin B12'), help_text=_('µg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_c = models.DecimalField(verbose_name=_('Vitamin C'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_d = models.DecimalField(verbose_name=_('Vitamin D'), help_text=_('µg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_e = models.DecimalField(verbose_name=_('Vitamin E'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    vitamins_pp = models.DecimalField(verbose_name=_('Vitamin PP'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_zinc = models.DecimalField(verbose_name=_('Zinc'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_phosphorus = models.DecimalField(verbose_name=_('Phosphorus'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_iodine = models.DecimalField(verbose_name=_('Iodine'), help_text=_('µg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_magnesium = models.DecimalField(verbose_name=_('Magnesium'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_copper = models.DecimalField(verbose_name=_('Copper'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_potasium = models.DecimalField(verbose_name=_('Potasium'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_selenium = models.DecimalField(verbose_name=_('Selenium'), help_text=_('µg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_sodium = models.DecimalField(verbose_name=_('Sodium'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_calcium = models.DecimalField(verbose_name=_('Calcium'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)
    minerals_iron = models.DecimalField(verbose_name=_('Iron'), help_text=_('mg/100g'), decimal_places=2, max_digits=5, blank=True, null=True, default=None)

2.1.4. File encoding

  • UTF-8

  • always remember to open files for reading and writing with encoding='utf-8'

  • All identifiers in the Python standard library MUST use ASCII-only identifiers, and SHOULD use English words wherever feasible (in many cases, abbreviations and technical terms are used which aren't English).

  • String literals and comments must also be in ASCII.

  • Authors whose names are not based on the Latin alphabet (latin-1, ISO/IEC 8859-1 character set) MUST provide a transliteration of their names in this character set.

2.1.5. Comments

  • Comments that contradict the code are worse than no comments.

  • Comments should be complete sentences.

  • Block comments generally consist of one or more paragraphs built out of complete sentences

  • Each sentence ending in a period.

  • Python coders from non-English speaking countries: please write your comments in English, unless you are 120% sure that the code will never be read by people who don't speak your language.

  • Each line of a block comment starts with a # and a single space (unless it is indented text inside the comment).

2.1.6. Documentation Strings

  • PEP 257 -- Docstring Conventions

  • Write docstrings for all public modules, functions, classes, and methods.

  • Docstrings are not necessary for non-public methods, but you should have a comment that describes what the method does.

  • For one liner docstrings, please keep the closing """ on the same line.

  • PEP 257 -- Docstring Conventions: For multiline str always use three double quote (""") characters

2.1.7. Use better names, rather than comments

def cal_var(data):
    """Calculate variance"""
    return sum((Xi-m) ** 2 for Xi in data) / len(data)

def calculate_variance(data):
    return sum((Xi-m) ** 2 for Xi in data) / len(data)
def fabs(a, b):
    return float(abs(a + b))

def float_absolute_value(a, b):
    return float(abs(a + b))

def abs(a: int, b: int) -> float:
    return float(abs(a + b))

def absolute_value(a: int, b: int) -> float:
    return float(abs(a + b))

2.1.8. Commented code?

  • NO!

  • Never commit files with commented code

2.1.9. Author name or revision version

  • Do not put author name or revision version to the files

  • Version Control System is responsible for that

2.1.10. Naming convention

Constants and Variables:

  • Używanie _ w nazwach (snake_case) - // Python - snake ;)

  • variable or variable_name

    name = 'José Jiménez'
    
    firstname = 'José'
    lastname = 'Jiménez'
    
  • CONSTANT or CONSTANT_NAME

    PATH = '/etc/hosts'
    
    FILE_NAME = 'README.txt'
    

Classes:

  • PascalCase

    class MyClass:
        pass
    

2.1.11. Class Attributes

  • Public attributes should have no leading underscores.

  • If your public attribute name collides with a reserved keyword, append a single trailing underscore to your attribute name.

  • cls is the preferred spelling for any variable or argument which is known to be a class, especially the first argument to a class method.

2.1.12. Methods/Functions

  • Używanie _ w nazwach (snake_case) - // Python - snake ;)

  • method_name() or function_name()

    def add_numbers(a, b):
        return a + b
    
  • Nie robimy camelCase

    def addNumbers(a, b):
        return a + b
    

2.1.13. Modules names

  • modulename

  • module_name

  • Preferable one word

    import random
    import argparse
    

2.1.14. Function/Method argument names

  • self

    class Astronaut:
        name = 'José Jiménez'
    
        def say_hello(self):
            print(f'My name... {self.name}')
    
  • cls

    class Astronaut:
        pass
    
    class Cosmonaut:
        pass
    
    class Starman:
        pass
    
    def is_spaceman(cls):
        if instance(cls, (Astronaut, Cosmonaut)):
            return True
        else:
            return False
    
    
    is_spaceman(Cosmonaut)  # True
    is_spaceman(Astronaut)  # True
    is_spaceman(Starman)    # False
    
  • self and other

    class Vector:
        x = 0
        y = 1
    
        def __add__(self, other):
            return Vector(
                x=self.x+other.x,
                y=self.y+other.y
            )
    

2.1.15. Using __ and _ in names

  • W Pythonie nie ma private/protected/public

  • Funkcje o nazwie zaczynającej się od _ przez konwencję są traktowane jako prywatne

    from random import _ceil
    
    _ceil()
    # good IDE will display information, that you're accessing protected member
    
  • Funkcje i zmienne o nazwie zaczynającej się od __ i kończących się na __ przez konwencję są traktowane jako systemowe

    print(__file__)
    
  • _ at the end of name when name collision

    def print_(text1, text2):
        print(values, sep=';', end='\n')
    

2.1.16. Single or double quotes?

  • Python nie rozróżnia czy stosujemy pojedyncze znaki cudzysłowu czy podwójne.

  • Ważne jest aby wybrać jedną konwencję i się jej konsekwentnie trzymać.

  • Interpreter Pythona domyślnie stosuje pojedyncze znaki cudzysłowu.

  • Z tego powodu w tej książce będziemy trzymać się powyższej konwencji.

  • Ma to znaczenie przy doctest, który zawsze korzysta z pojedynczych i rzuca errorem jak są podwójne

  • PEP 257 -- Docstring Conventions: For multiline str always use three double quote (""") characters

print('It\'s Watney\'s Mars')
print("It is Watney's Mars")
print('<a href="https://python3.info">Python and Machine Learning</a>')

2.1.17. Trailing Commas

Yes:

FILES = ('setup.cfg',)

OK, but confusing:

FILES = 'setup.cfg',

2.1.18. Indents

Good:

# More indentation included to distinguish this from the rest.
def server(
        host='example.com', port=443, secure=True,
        username='myusername', password='mypassword'):
    return locals()


# Aligned with opening delimiter.
connection = server(host='example.com', port=443, secure=True,
                    username='myusername', password='mypassword')

# Hanging indents should add a level.
connection = server(
    host='example.com', port=443, secure=True,
    username='myusername', password='mypassword')

# The best
connection = server(
    host='example.com',
    username='myusername',
    password='mypassword',
    port=443,
    secure=True,
)

Bad:

# Further indentation required as indentation is not distinguishable.
def Connection(
    host='example.com', port=1337,
    username='myusername', password='mypassword'):
    return host, port, username, password


# Arguments on first line forbidden when not using vertical alignment.
connection = Connection(host='example.com', port=1337,
    username='myusername', password='mypassword')

2.1.19. Brackets

vector = [
    1, 2, 3,
    4, 5, 6,
]

result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)

vector = [
    1, 2, 3,
    4, 5, 6]

result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f')
TYPE_CHOICES = [
    ('custom', _('Custom Made')),
    ('brand', _('Brand Product')),
    ('gourmet', _('Gourmet Food')),
    ('restaurant', _('Restaurant'))]

FORM_CHOICES = [
    ('solid', _('Solid')),
    ('liquid', _('Liquid'))]

CATEGORY_CHOICES = [
    ('other', _('Other')),
    ('fruits', _('Fruits')),
    ('vegetables', _('Vegetables')),
    ('meat', _('Meat'))]

2.1.20. Modules

  • Modules should explicitly declare the names in their public API using the __all__ attribute.

  • Setting __all__ to an empty list indicates that the module has no public API.

2.1.21. Line continuation

Linie możemy łamać poprzez stawianie znaku ukośnika \ na końcu:

with open('/path/to/some/file/you/want/to/read') as file1, \
        open('/path/to/some/file/being/written', mode='w') as file2:
    content = file1.read()
    file2.write(content)

Easy to match operators with operands:

income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)
class Server:
    def __init__(self, username, password, host='example.com'
                 port=80, secure=False):

        if not instance(username, str) or not instance(password, str) or
                not instance(host, str) or not instance(secure, bool) or
                (not instance(port, int) and 0 < port <= 65535):
            raise TypeError(f'One of your parameters is incorrect type')

     def __str__(self):
        if secure:
            protocol = 'https'
        else:
            protocol = 'http'

        return f'{protocol}://{self.username}:{self.password}@{self.host}:{self.port}/'

server = Server(
    host='example.com',
    username='myusername',
    password='mypassword',
    port=443,
    secure=True,
)

2.1.22. Blank lines

  • Surround top-level function and class definitions with two blank lines.

  • Method definitions inside a class are surrounded by a single blank line.

  • Extra blank lines may be used (sparingly) to separate groups of related functions.

  • Use blank lines in functions, sparingly, to indicate logical sections.

class Server:
    def __init__(self, username, password, host='example.com'
                 port=80, secure=False):

        if not instance(username, str):
            raise TypeError(f'Username must be str')

        if not instance(password, str):
            raise TypeError(f'Password must be str')

        if not instance(port, int):
            raise TypeError(f'Port must be int')
        elif: 0 < port <= 65535
            raise ValueError(f'Port must be 0-65535')

    def __str__(self):
        if secure:
            protocol = 'https'
        else:
            protocol = 'http'

        return f'{protocol}://{self.username}:{self.password}@{self.host}:{self.port}/'

2.1.23. Whitespace in function calls

spam(ham[1], {eggs: 2})        # Good
spam( ham[ 1 ], { eggs: 2 } )  # Bad
spam(1)     # Good
spam (1)    # Bad
do_one()    # Good
do_two()    # Good
do_three()  # Good

do_one(); do_two(); do_three()                  # Bad

do_one(); do_two(); do_three(long, argument,    # Bad
                             list, like, this)  # Bad

2.1.24. Whitespace in slices

ham[1:9]                          # Good
ham[1:9:3]                        # Good
ham[:9:3]                         # Good
ham[1::3]                         # Good
ham[1:9:]                         # Good

ham[1: 9]                         # Bad
ham[1 :9]                         # Bad
ham[1:9 :3]                       # Bad
ham[lower:upper]                  # Good
ham[lower:upper:]                 # Good
ham[lower::step]                  # Good

ham[lower : : upper]              # Bad
ham[lower+offset : upper+offset]  # Good
ham[: upper_fn(x) : step_fn(x)]   # Good
ham[:: step_fn(x)]                # Good

ham[lower + offset:upper + offset]    # Bad
ham[:upper]             # Good
ham[ : upper]           # Bad
ham[ :upper]            # Bad

2.1.25. Whitespace in assignments

x = 1                   # Good
y = 2                   # Good
long_variable = 3       # Good

x             = 1       # Bad
y             = 2       # Bad
long_variable = 3       # Bad
i = i + 1               # Good
i=i+1                   # Bad
submitted += 1          # Good
submitted +=1           # Bad

2.1.26. Whitespace in math operators

x = x*2 - 1             # Good
x = x * 2 - 1           # Bad
hypot2 = x*x + y*y      # Good
hypot2 = x * x + y * y  # Bad
c = (a+b) * (a-b)      # Good
c = (a + b) * (a - b)  # Bad

2.1.27. Whitespace in accessors

dct['key'] = lst[index]     # Good
dct ['key'] = lst[ index ]  # Bad

2.1.28. Whitespace in functions

Good:
def complex(real, imag=0.0):
    return magic(r=real, i=imag)
Bad:
def complex(real, imag = 0.0):
    return magic(r = real, i = imag)
Controversial:
def move(self, left: int = 0, down: int = 0, up: int = 0, right: int = 0) -> None:
    self.set_position_coordinates(
        x=self.position.x + right - left,
        y=self.position.y + down - up
    )

2.1.29. Whitespace in conditionals

Good:
if foo == 'blah':
    do_blah_thing()
Bad:
if foo == 'blah': do_blah_thing()

if foo == 'blah': one(); two(); three()

if foo == 'blah': do_blah_thing()
else: do_non_blah_thing()

2.1.30. Whitespace in exceptions

Good:
try:
    do_something()
except Exception:
    pass
Bad:
try: something()
finally: cleanup()

2.1.31. Conditionals

Good:
if greeting:
    pass
Bad:
if greeting == True:
    pass

if greeting is True:
    pass

2.1.32. Negative Conditionals

Good:
if name is not None:
    pass
Bad:
# if (! name == null) {}
if not name is None:
    pass
usernames = {'mwatney', 'mlewis', 'rmartinez'}

# if (! usernames.contains('José')) {}
if not 'mwatney' in usernames:
    print('I do not know you')
else:
    print('Hello my friend')

2.1.33. Checking if not empty

Good:
if sequence:
    pass

if not sequence:
    pass
Bad:
if len(sequence):
    pass

if not len(sequence):
    pass

2.1.34. Explicit return

Good:
def foo(x):
    if x >= 0:
        return math.sqrt(x)
    else:
        return None
Bad:
def foo(x):
    if x >= 0:
        return math.sqrt(x)

2.1.35. Explicit return value

Good:
def bar(x):
    if x < 0:
        return None
    return math.sqrt(x)
Bad:
def bar(x):
    if x < 0:
        return
    return math.sqrt(x)

2.1.36. Imports

  • Każdy z importów powinien być w osobnej linii

  • importy systemowe na górze

  • importy bibliotek zewnętrznych poniżej systemowych

  • importy własnych modułów poniżej bibliotek zewnętrznych

  • jeżeli jest dużo importów, pomiędzy grupami powinna być linia przerwy

Good:
import os
import sys
import requests
import numpy as np
from datetime import date
from datetime import time
from datetime import datetime
from datetime import timezone
from datetime import date, time, datetime, timezone
from datetime import date, time, datetime, timezone
import os
import sys
from random import shuffle
from subprocess import Popen, PIPE
import requests
import numpy as np
Bad:
import sys, os, requests, numpy
import sys, os
import requests, numpy

2.1.37. Whitespace with type annotations

Good:
def function(first: str):
    pass

def function(first: str = None):
    pass

def function() -> None:
    pass

def function(first: str, second: str = None, limit: int = 1000) -> int:
    pass
Bad:
def function(first: str=None):
    pass

def function(first:str):
    pass

def function(first: str)->None:
    pass

2.1.38. Magic number i magic string

  • NO!

2.1.39. PEP 8, but...

2.1.40. Static Code Analysis

  • More information in cicd-tools

  • More information in cicd-pipelines

2.1.41. pycodestyle

  • Previously known as pep8

  • Python style guide checker.

  • pycodestyle is a tool to check your Python code against some of the style conventions in PEP 8

  • Plugin architecture: Adding new checks is easy

  • Parseable output: Jump to error location in your editor

  • Small: Just one Python file, requires only stdlib

  • Comes with a comprehensive test suite

Installation:

$ pip install pycodestyle

Usage:

$ pycodestyle FILE.py
$ pycodestyle DIRECTORY/*.py
$ pycodestyle DIRECTORY/
$ pycodestyle --statistics -qq DIRECTORY/
$ pycodestyle --show-source --show-pep8 FILE.py

Configuration:

  • setup.cfg

[pycodestyle]
max-line-length = 120
ignore = E402,W391

2.1.42. Assignments

Code 2.25. Solution
"""
* Assignment: DevOps PEP8 Pycodestyle
* Complexity: easy
* Lines of code: 2 lines
* Time: 5 min

English:
    1. Install `pycodestyle`
    2. Run `pycodestyle` on your last script
    3. Fix all errors
    4. Run doctests - all must succeed

Polish:
    1. Install `pycodestyle`
    2. Uruchom `pycodestyle` na swoim ostatnim skrypcie
    3. Popraw wszystkie błędy
    4. Uruchom doctesty - wszystkie muszą się powieść
"""