Skip to content

Instantly share code, notes, and snippets.

@sloria
Last active November 14, 2024 15:01
Show Gist options
  • Save sloria/7001839 to your computer and use it in GitHub Desktop.
Save sloria/7001839 to your computer and use it in GitHub Desktop.
A "Best of the Best Practices" (BOBP) guide to developing in Python.

The Best of the Best Practices (BOBP) Guide for Python

A "Best of the Best Practices" (BOBP) guide to developing in Python.

In General

Values

  • "Build tools for others that you want to be built for you." - Kenneth Reitz
  • "Simplicity is alway better than functionality." - Pieter Hintjens
  • "Fit the 90% use-case. Ignore the nay sayers." - Kenneth Reitz
  • "Beautiful is better than ugly." - PEP 20
  • Build for open source (even for closed source projects).

General Development Guidelines

  • "Explicit is better than implicit" - PEP 20
  • "Readability counts." - PEP 20
  • "Anybody can fix anything." - Khan Academy Development Docs
  • Fix each broken window (bad design, wrong decision, or poor code) as soon as it is discovered.
  • "Now is better than never." - PEP 20
  • Test ruthlessly. Write docs for new features.
  • Even more important that Test-Driven Development--Human-Driven Development
  • These guidelines may--and probably will--change.

In Particular

Style

Follow PEP 8, when sensible.

Naming

  • Variables, functions, methods, packages, modules
    • lower_case_with_underscores
  • Classes and Exceptions
    • CapWords
  • Protected methods and internal functions
    • _single_leading_underscore(self, ...)
  • Private methods
    • __double_leading_underscore(self, ...)
  • Constants
    • ALL_CAPS_WITH_UNDERSCORES
General Naming Guidelines

Avoid one-letter variables (esp. l, O, I).

Exception: In very short blocks, when the meaning is clearly visible from the immediate context

Fine

for e in elements:
    e.mutate()

Avoid redundant labeling.

Yes

import audio

core = audio.Core()
controller = audio.Controller()

No

import audio

core = audio.AudioCore()
controller = audio.AudioController()

Prefer "reverse notation".

Yes

elements = ...
elements_active = ...
elements_defunct = ...

No

elements = ...
active_elements = ...
defunct_elements ...

Avoid getter and setter methods.

Yes

person.age = 42

No

person.set_age(42)

Indentation

Use 4 spaces--never tabs. Enough said.

Imports

Import entire modules instead of individual symbols within a module. For example, for a top-level module canteen that has a file canteen/sessions.py,

Yes

import canteen
import canteen.sessions
from canteen import sessions

No

from canteen import get_user  # Symbol from canteen/__init__.py
from canteen.sessions import get_session  # Symbol from canteen/sessions.py

Exception: For third-party code where documentation explicitly says to import individual symbols.

Rationale: Avoids circular imports. See here.

Put all imports at the top of the page with three sections, each separated by a blank line, in this order:

  1. System imports
  2. Third-party imports
  3. Local source tree imports

Rationale: Makes it clear where each module is coming from.

Documentation

Follow PEP 257's docstring guidelines. reStructured Text and Sphinx can help to enforce these standards.

Use one-line docstrings for obvious functions.

"""Return the pathname of ``foo``."""

Multiline docstrings should include

  • Summary line
  • Use case, if appropriate
  • Args
  • Return type and semantics, unless None is returned
"""Train a model to classify Foos and Bars.

Usage::

    >>> import klassify
    >>> data = [("green", "foo"), ("orange", "bar")]
    >>> classifier = klassify.train(data)

:param train_data: A list of tuples of the form ``(color, label)``.
:rtype: A :class:`Classifier <Classifier>`
"""

Notes

  • Use action words ("Return") rather than descriptions ("Returns").
  • Document __init__ methods in the docstring for the class.
class Person(object):
    """A simple representation of a human being.

    :param name: A string, the person's name.
    :param age: An int, the person's age.
    """
    def __init__(self, name, age):
        self.name = name
        self.age = age
On comments

Use them sparingly. Prefer code readability to writing a lot of comments. Often, small methods are more effective than comments.

No

# If the sign is a stop sign
if sign.color == 'red' and sign.sides == 8:
    stop()

Yes

def is_stop_sign(sign):
    return sign.color == 'red' and sign.sides == 8

if is_stop_sign(sign):
    stop()

When you do write comments, remember: "Strunk and White apply." - PEP 8

Line lengths

Don't stress over it. 80-100 characters is fine.

Use parentheses for line continuations.

wiki = (
    "The Colt Python is a .357 Magnum caliber revolver formerly manufactured "
    "by Colt's Manufacturing Company of Hartford, Connecticut. It is sometimes "
    'referred to as a "Combat Magnum". It was first introduced in 1955, the '
    "same year as Smith & Wesson's M29 .44 Magnum."
)

Testing

Strive for 100% code coverage, but don't get obsess over the coverage score.

General testing guidelines

  • Use long, descriptive names. This often obviates the need for doctrings in test methods.
  • Tests should be isolated. Don't interact with a real database or network. Use a separate test database that gets torn down or use mock objects.
  • Prefer factories to fixtures.
  • Never let incomplete tests pass, else you run the risk of forgetting about them. Instead, add a placeholder like assert False, "TODO: finish me".

Unit Tests

  • Focus on one tiny bit of functionality.
  • Should be fast, but a slow test is better than no test.
  • It often makes sense to have one testcase class for a single class or model.
import unittest
import factories

class PersonTest(unittest.TestCase):
    def setUp(self):
        self.person = factories.PersonFactory()

    def test_has_age_in_dog_years(self):
        self.assertEqual(self.person.dog_years, self.person.age / 7)

Functional Tests

Functional tests are higher level tests that are closer to how an end-user would interact with your application. They are typically used for web and GUI applications.

  • Write tests as scenarios. Testcase and test method names should read like a scenario description.
  • Use comments to write out stories, before writing the test code.
import unittest

class TestAUser(unittest.TestCase):

    def test_can_write_a_blog_post(self):
        # Goes to the her dashboard
        ...
        # Clicks "New Post"
        ...
        # Fills out the post form
        ...
        # Clicks "Submit"
        ...
        # Can see the new post
        ...

Notice how the testcase and test method read together like "Test A User can write a blog post".

Inspired by...

License

Licensed under the CC-BY 4.0 License

@giacomomarchioro
Copy link

Good job, but I don't agree with using person.age = 42 instead person.set_age(42) in many cases using a setter method allows adding more checks and documentation on how to add the property correctly, overloading the setter method is often unpractical. I really don't see where is the problem creating a setter method...

Why would you say that overloading the setter method is often unpractical. Could you elaborate on cases where you would prefer writing a setter yourself over using a property setter?

@andrewcrook pointed out a typical example. Of course, there are some cases in which could be recommended to use property but in many years of programming, I found that in many cases the set_method was a better solution for me. For instance, often I needed to modify the attribute without the validation for special cases and exceptions. In this case, having access to the attribute without restrictions was very handy. Then compare the two solutions:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        assert value.isdigit(),"Not a digit"
        self._x = value


c  = C()
c.x = '123'
c.x = 'test'

class C(object):
    def __init__(self):
        self.x = None

    def set_x(self,value):
        assert value.isdigit(),"Not a digit"
        self.x = value


c  = C()
c.x = '123'
c.x = 'test'

Imagine writing a library with hundreds of setters for validation purposes, creating setter as a function was absolutely advantageous.

@jannismain
Copy link

jannismain commented Jul 21, 2022

@andrewcrook That makes sense! :) If you need more than the simple setter provided by @property.setter, you have to write a custom setter method.

One downside of setter methods without a property is that you don't prevent accidentally reassigning the property directly c.x = '123'. If somebody doesn't know that set_x('123') is the proper way to assign that value, he won't benefit from the checks you provide in your setter method.

@giacomomarchioro I agree that the @property syntax is a little more verbose than providing a custom setter-only. However, you are missing a custom getter in order to have feature-parity with the property approach. And even then, the same problem with accidentally assigning the property directly holds. If you want to directly assign, you can do c._x in your example, which clearly indicates you are doing something that might break other things.

Personally, I will only resort to setter methods, when I need behavior as shown by @andrewcrook 's example. And even then, I might still use a property to have control over what happens when somebody assigns a value directly.

@andrewcrook
Copy link

andrewcrook commented Jul 21, 2022

directly c.x = '123'. If somebody doesn't know that set_x('123')

yeah private variables are more of a convention but I think everyone understands _x

Personally I would use encapsulation by convention for internal class so the only way to change or read a value is from the given class method or getter/setter. I don't know if static checkers can enforce private variables using _x, mypy maybe? I would have to check.

However, if it's just a simple class which holds data to save on having to write a lot of boilerplate and a lot of repetition for a class. I would use dataclasses, if I needed better type conversion and validation then I would use attrs (downside is its not in the standard library). These examples can also use __Slots__ ('dataclasses`since Python 3.10 iirc?) and are not hugely resource expensive when compared to a class.

@andrewcrook
Copy link

andrewcrook commented Jul 21, 2022

@giacomomarchioro I think youth might find this video interesting with regards Python Vs Java

https://archive.org/details/SeanKellyRecoveryfromAddiction

@jannismain why not "have your cake and eat it”?

a property using setters and getters is ideal for refactoring legacy Python.

another example

# circle.py

class Circle:
    def __init__(self, radius):
        self._radius = radius

    def _get_radius(self):
        print("Get radius")
        return self._radius

    def _set_radius(self, value):
        print("Set radius")
        self._radius = value

    def _del_radius(self):
        print("Delete radius")
        del self._radius

    radius = property(
        fget=_get_radius,
        fset=_set_radius,
        fdel=_del_radius,
        doc="The radius property."
    )

-- realpython

@jannismain
Copy link

When you're assigning your private setters and getters to a property anyways, why not use the property decorators then? What are you gaining?

# circle.py

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        "The radius property."
        print("Get radius")
        return self._radius

    @radius.setter
    def radius(self, value):
        print("Set radius")
        self._radius = value

    @radius.deleter
    def radius(self):
        print("Delete radius")
        del self._radius

@andrewcrook
Copy link

andrewcrook commented Jul 25, 2022

@jannismain Sorry yes, I see what you mean. You can use the property decorator over property() its exactly the same. I just thought the former was easier to explain by showing that either the getter and setter are called from it. I think direct attributes are the way to go but as soon as you need any validation, conversions I think private getters and setters; and a property decorator are the best way forward. The beautiful thing is you can choose to introduce getters and setters without having to alter the code using the attributes by moving to properties. In Java you had to make the decision early.

Personally, I don't like to see blocks of code which are loads of lines updating or getting data from multiple attribute/properties. When you can do it using methods accepting multiple arguments or using a data structure. The common example is a row of data from a database.

Methods for accepting different interpretations of information or combined information such as years and months are needed because the alternative such as fractional years or using the smallest unit is horrible. So you probably want to convert via a method and then encapsulate in the class using lowest unit required e.g. months or days.

person.set_age(years=42, months= 5)

As I said you can also make savings and beautify code using some of the alternatives to classes such as dataclasses and attrs.

@jannismain
Copy link

jannismain commented Jul 26, 2022

@andrewcrook I definitively see your point when working with legacy Python. Also, your snippet was a nice reminder that the decorator syntax is not the only way to get to properties :) And finally, when you want or need arguments, adding a getter/setter method (maybe in addition to your property) is unavoidable. I like the person.set_age example!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment