Python Like PRO Light Mode
Python Like PRO Light Mode
steven07kolesiko@gmail.com
Contents
Preface 3
What makes you good . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Appreciation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Chapter 1: Projects 5
Guidelines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Structure your project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
The reasoning behind a src directory . . . . . . . . . . . . . . . . . . . . . . . . . . 5
How to name files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Real life example when naming modules . . . . . . . . . . . . . . . . . . . . . . . . 8
Naming classes, functions, and variables . . . . . . . . . . . . . . . . . . . . . . . . 9
When to create a function or a class? . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Creating modules and entry points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Defining main for modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Structure your code after the problem it solves . . . . . . . . . . . . . . . . . . . . . . . 15
Don’t structure it after patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
When importing packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Making repetitive imports less boring . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Managing dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Stop using requirements.txt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Poetry: the red pill . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
pyproject.toml vs requirements.txt . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Install your code locally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Helper scripts and tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Dealing with simple scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Dealing with complex scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Use Linters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Linter recommendations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Recommended settings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Enforce linters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Directory tree from a real company . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Templates for you . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2
Preface
What makes you good
“ There are 10 kinds of people. Those who understand binary and
those who doesn’t. ”
What I don’t like about this phrase is that while it’s funny for those who understand that 10 is the
binary representation for 2 , it lets the other group of people (those who don’t know it) clueless.
Notice how each number can be summed up producing a total different decimal number:
When set 0 0 0 0 0 0 1 0
Then sum 128 64 32 16 8 4 +2 1
When set 0 0 0 0 0 0 1 1
Then sum 128 64 32 16 8 4 +2 +1
When set 0 0 0 0 0 1 0 1
Then sum 128 64 32 16 8 +4 2 +1
Hopefully, it makes sense now and that it wouldn’t be hard for you to find out the decimal for 1010
.
Analogously I want to give you meaningful instructions so Python gets as easy as what you just read.
“ Any fool can write code that a computer can understand. Good
programmers write code that humans can understand. ” - Martin
Fowler
Again we split the reader into two possible groups: fools and good programmers.
I can’t tell you who you are, but I can guarantee you that I want to transform you into the latter
group and I’ll use this book to do that.
If we keep thinking about it we realize that all good programmers start as fools.
We all need to write some code that can be read by the computer, but what will turn you into a good
programmer is the act of evolving until humans can understand it.
3
“ There are 2 kinds of people. Those who write code and those
who build software ”
Code is a hobby. Software is paid.
Code runs. Software is maintained.
Code doesn’t require much planning. Software is designed.
Code is hard to change. Software scales.
There might be many books around there teaching you to write Python, but I don’t see many
teaching people how to build software with Python.
You can bet I’m going to teach you how to build great software with Python.
In Python we call it the Pythonic way of doing things.
2022, July 04,
Guilherme Magalhães Latrova (aka Gui)
Appreciation
I walked many years feeling lost in life, career, and purpose. I thank God who found me and took
care of me.
I thank my wife Thais Cabral that joined me in taking many risks and made me a better man.
Jonathan Tolentino Abila who listened to my complaints and made the past years working with
terrible code bearable.
I thank David Grandes that (out of his normal senses) hired a 26 dude to be a tech lead for the first
time.
Finally, I thank Kwan Lee that didn’t send me away when I applied for a role requiring 10+ years of
experience having less than 5. Actually I’m grateful that he had a call with me, gave me a harder
challenge, and decided to hire me for a lower position. Thank you for giving me a shot.
4
Chapter 1: Projects
Guidelines
“ For every minute spent in organizing, an hour is earned. ” -
Benjamin Franklin
Python is different from languages like C# or Java where they enforce you to have classes named
after the file they live in.
So far Python is one of the most flexible languages I had contact with and everything too flexible
enhances the odds of bad decisions.
• Do you want to keep all project classes in a single main.py file? Yes, it works.
• Do you need to read an os environment var? Just read it right there.
• Do you need to modify a function behavior? Why not a decorator!?
Many decisions that are easy to implement may backfire producing code that is extremely
hard to maintain.
This is not necessarily bad if you know what you’re doing.
During this chapter, I’m going to present to you guidelines that worked for me over the past working
in different companies and with many different people.
Where <module> is your main module. If in doubt, consider what people would pip install and
how you would like to import module .
Frequently it has the same name as the top project. This isn’t a rule though.
5
This is quite annoying because of the lack of order, producing structures like pytorch:
pytorch
├── android
├── aten
├── benchmarks
├── binaries
├── c10
├── caffe2
├── cmake
├── docs
├── functorch
├── ios
├── modules
├── mypy_plugins
├── scripts
├── test
├── third_party
├── tools
├── torch
├── torchgen
├── aten.bzl
├── buckbuild.bzl
├── BUCK.oss
├── BUILD.bazel
├── build.bzl
├── build_variables.bzl
├── c2_defs.bzl
├── c2_test_defs.bzl
├── CITATION
├── CMakeLists.txt
├── CODE_OF_CONDUCT.md
├── CODEOWNERS
├── CONTRIBUTING.md
├── defs.bzl
├── defs_gpu.bzl
├── defs_hip.bzl
├── Dockerfile
├── docker.Makefile
6
├── GLOSSARY.md
├── LICENSE
├── Makefile
├── MANIFEST.in
├── mypy.ini
├── mypy-strict.ini
├── NOTICE
├── pt_ops.bzl
├── pt_template_srcs.bzl
├── pyproject.toml
├── pytest.ini
├── README.md
├── RELEASE.md
├── requirements-flake8.txt
├── requirements.txt
├── SECURITY.md
├── setup.py
├── ubsan.supp
├── ufunc_defs.bzl
├── version.txt
└── WORKSPACE
It’s boring to have things so apart due to the alphabetical sorting of the IDE.
The main reason behind the src dir is to keep active project code concentrated inside a single
directory while settings, CI/CD setup, and project metadata can reside outside of it. Take the lightning
AI as example.
The only drawback of doing it is that you can’t import module_a in your python code out of the
box. We need to set up the project to be installed under this repository. We’re going to discuss how
to solve this soon in this chapter.
Rule 1: Files are seen as modules First of all, in Python “files” are not just “files” and I noticed
this is the main source of confusion for beginners.
If you’re inside a directory that contains any __init__.py it’s a directory composed of modules,
not files.
See each module as a namespace.
I mean namespace because you can’t say for sure whether they have many functions, classes, or
just constants. It can have virtually all of them or just a bunch of some.
Rule 2: Keep things together as needed It’s fine to have several classes within a single
module, and you should do so. (when classes are related to the module, obviously.)
Only break it down when your module gets too big, or when it handles different concerns.
7
Figure 1: One does not simply create a file in Python
Often, people think it’s a bad practice due to some experience with other languages that enforce the
other way around (e.g. Java and C#).
Rule 3: By default give plural names As a rule of thumb, name your modules in the plural and
name them after a business context (more on it soon).
There’re exceptions to this rule though! Modules can be named core , main.py , and similar to
represent a single thing. Use your judgment, if in doubt stick to the plural rule.
8
│ ├── facades.py
│ ├── main.py ← (Singular)
│ └── storages.py
│
├── .gitignore
├── pyproject.toml
└── README.md
I can understand that I might have one or many exception classes inside exceptions and so on.
The beauty about having plural modules is that:
• They’re not too small (e.g. one per class)
• You can at any moment break it down into smaller modules if required
• They give you a strong sense of knowing what might exist inside
Some people claim naming things is hard. It gets less hard when you define some guidelines.
Functions and Methods should be verbs Functions and methods represent an action or action-
able stuff. Something “isn’t”. Something is “happening”.
Actions are clearly stated by verbs.
A few good examples from REAL projects I worked on before:
def get_orders():
...
def acknowledge_event():
...
def get_delivery_information():
...
def publish():
...
def api_call():
...
def specific_stuff():
...
9
They’re a bit unclear whether they return an object to allow me to perform the api call or if it actually
send an email for example.
I can picture a scenario like this:
# Example of a misleading function name
email_send.title = "title"
email_send.dispatch()
Variables and Constants should be nouns Should always be nouns, never verbs (which clarifies
the difference between functions).
Good examples:
plane = Plane()
customer_id = 5
KEY_COMPARISON = "abc"
Bad examples:
fly = Plane()
get_customer_id = 5
COMPARE_KEY = "abc"
Classes should be self explanatory, but Suffixes are fine Prefer classes with self explanatory
names. It’s fine to have suffixes like Service , Strategy , Middleware , but only when extremely
necessary to make its purpose clear.
Always name it in singular instead of plural. Plural reminds us of collections (e.g. if I read orders
I assume it’s a list or iterable), so remind yourself that once a class is instantiated it becomes a single
object.
Classes representing entities
Classes that represent things from the business context should be named as is (nouns!). Like Order
, Sale , Store , Restaurant and so on.
Example of suffixes usage
Let’s consider you want to create a class responsible for sending emails. If you name it just as “
Email ”, its purpose is not clear.
Someone might think it may represent an entity e.g.
email = Email() # inferred usage example
email.title = "Title"
email.body = create_body()
email.send_to = "guilatrova.dev"
send_email(email)
10
You should name it “ EmailSender ” or “ EmailService ”.
Even though Python doesn’t support constants we can still use mypy to enforce the value won’t
change (more on type hinting later).
Disclaimer about “““private”“” methods. Some people found out that if you have
__method(self) (any method starting with two underscores) Python won’t let outside
classes/methods invoke it normally which leads them to think it’s fine.
The leading __ is built to ensure subclassers can’t override that method, so subclasses can’t even
call super().__method which may be confusing.
Take this code as example:
class ParentClass:
def call_me(self) -> str:
return self.__call_me_maybe()
class ChildClass(ParentClass):
def call_me(self) -> str:
return self.__call_me_maybe() + ( # ← Exception :(
"You took your time with the call"
"I took no time with the fall"
"You gave me nothing at all"
)
It gives as output:
Parent call_me output:
Hey, I just met you, and this is crazy
But here's my code, so call me, maybe
11
Traceback (most recent call last):
File "private_dunder.py", line 25, in <module>
print(ChildClass().call_me())
File "private_dunder.py", line 13, in call_me
return self.__call_me_maybe() + ( # ← Exception :(
AttributeError: 'ChildClass' object has no attribute '_ChildClass__call_me_maybe'.
Did you mean: '_ParentClass__call_me_maybe'?
If you came from a C# environment like myself it might sound weird that you can’t protect a method.
But Guido (Python’s creator) has a good reason behind it:
“We’re all consenting adults here”
It means that if you’re aware you shouldn’t be invoking a method, then you shouldn’t unless you
know what you’re doing.
After all if you really decided to invoke that method, you’re going to do something dirty to make it
happen (known as “Reflection” in C#).
Mark your private method/function with a single initial underscore to state it’s intended for private
use only and live with it.
12
If you follow the above recommendations you’re going to have clear modules and clear modules are
an effective way to organize functions:
from gmaps_crawler import storages
storages.get_storage() # ← Similar to a class, except it's not instantied and has a plural na
storages.save_to_storage() # ← Potential function inside module
Sometimes you can identify subsets of functions inside a module. When this happens a class makes
more sense:
Example on grouping different subset of functions Consider the same storages module
with 4 functions:
def format_for_debug(some_data):
...
def save_debug(some_data):
"""Prints in the screen"""
formatted_data = format_for_debug(some_data)
print(formatted_data)
def create_s3(bucket):
"""Create s3 bucket if it doesn't exists"""
...
def save_s3(some_data):
s3 = create_s3("bucket_name")
...
S3 is a cloud storage to store any sort of data provided by Amazon (AWS). It’s like Google Drive for
software.
We can say that:
• The developer can save data in DEBUG mode (that just prints on the screen) or on S3 (that
stores data on the cloud).
• save_debug uses the format_for_debug function
• save_s3 uses the create_s3 function
I can see two groups of functions and no reason to keep them in different modules as they seem
small, thus I’d enjoy having them defined as classes:
class DebugStorage:
def format_for_debug(self, some_data):
...
class S3Storage:
def create_s3(self, bucket):
"""Create s3 bucket if it doesn't exists"""
...
13
def save_s3(self, some_data):
s3 = self.create_s3("bucket_name")
...
By doing that you ensure that any imports won’t trigger your code by accident. Unless it’s
explicitly executed.
You might have noticed some python packages that can be invoked by passing down -m like:
python -m pytest
python -m tryceratops
python -m faust
python -m flake8
python -m black
Such packages are treated almost like regular commands since you can also run them as:
pytest
tryceratops
faust
flake8
black
To make this happen you need to specify a single __main__.py file inside your main module:
<project>
├── src
│ ├── example_module ← Main module
│ │ ├── __init__.py
│ │ ├── __main__.py ← Add it here
│ │ └── many_files.py
│ │
│ └── tests/*
14
│ └── many_tests.py
│
├── .gitignore
├── pyproject.toml
└── README.md
Don’t forget you still need to include the check __name__ == "__main__" inside your __main__.py
file.
When you install your module, you can run your project as python -m example_module .
NOTE: If you’re willing to create a CLI and distribute it, you should follow these recommendations.
We’re going to use Typer later in this book to achieve that (TODO: Link).
Domain Description
Orders Defines the Order class, handles Product creation and stores them in the
database
Sales Defines the Sale class, which process orders, prints them, and syncs with the IRS
system
Restaurants Manages Restaurant classes, syncs opening hours to apps like DoorDash, and
manages stock for specific products
Note the way I grouped is arbitrary based on my vision of the problem. You could have defined
that Product is relevant enough to have its own module. Alternatively, you could have defined
restaurants are too tied to every sale, thus they should be together.
Even better: Always start with a single module named after the biggest problem (for this
example either orders or sales), evolve as it gets big/handles too many concerns.
Considering our system got large that’s how I see it:
It makes wonders to name modules after their purpose or design pattern (TODO: later in
this book add links).
Given the problem above I’d like to challenge you to guess what would reside inside each module.
It’d be fine if you’re not familiar with some design patterns like the mediator, we’re going to know a
few later in this book.
sales_proj
├── src
│ └── sales_proj
│ ├── __init__.py
│ ├── __main__.py
│ │
15
Figure 3: Problem structure
│ ├── orders
│ │ ├── models
│ │ │ ├── orders.py
│ │ │ └── products.py
│ │ │
│ │ └── exceptions.py
│ │
│ ├── restaurants
│ │ ├── services.py
│ │ ├── clients.py
│ │ └── models.py
│ │
│ └── sales
│ ├── services
│ │ ├── products.py
│ │ └── printers.py
│ │
│ ├── mediators.py
│ └── exceptions.py
│
├── .gitignore
16
├── pyproject.toml
└── README.md
This idea reminds me of the MVC pattern where you would split your code into Model, View, and
Controller.
Back when I worked with this pattern, many changes happening inside the View would impact the
Model, and finally the Controller. It was annoying to keep bouncing back and forth between distant
files.
Using our example it would represent:
antipattern
├── src
│ └── antipattern
│ ├── __init__.py
│ ├── __main__.py
│ │
│ ├── models
│ │ ├── orders.py
│ │ ├── restaurants.py
│ │ └── products.py
│ │
│ ├── exceptions
│ │ ├── restaurants.py
│ │ ├── sales.py
│ │ └── orders.py
│ │
17
│ ├── services
│ │ ├── sales
│ │ │ ├── products.py
│ │ │ └── printers.py
│ │ └── restaurants.py
│ │
│ └── (potentially more files or modules)
│
├── .gitignore
├── pyproject.toml
└── README.md
...
Even though the above code works it’s hard to know where these classes came from.
Here’s a rule of thumb to decide when to import packages:
• Only use relative imports when the file is in the same folder;
• For everything else, be explicit;
So considering we had the following:
# File located in src/managers/services/handlers/issues.py
from .clients import MyClient
from .models import MyModel
I can tell you right away that clients.py and models.py live side by side with issues.py , so
they’re located inside src/managers/services/handlers .
When organizing code it might feel like we’re sacrificing typing speed in favor of organization.
There’s a trick to overcome this.
Let’s imagine yet another scenario.
18
Inside our gmaps_crawler project we started having too many entities and it’s time to break it
down into three smaller modules: places.py , restaurants.py , reviews.py .
gmaps_crawler
├── src
│ └── gmaps_crawler
│ ├── __init__.py
│ ├── config.py
│ ├── drivers.py
│ ├── entities ← Module became a dir
│ │ ├── places.py
│ │ ├── restaurants.py
│ │ └── reviews.py
│ │
│ ├── exceptions.py
│ ├── facades.py
│ ├── main.py
│ └── storages.py
│
├── .gitignore
├── pyproject.toml
└── README.md
It would cause code like:
from gmaps_crawler.entities import Place, Restaurant, Review
To become:
from gmaps_crawler.entities.places import Place
from gmaps_crawler.entities.restaurants import Restaurant
from gmaps_crawler.entities.reviews import Review
That sucks, right? We just wanted to reorganize! Not rewrite the imports.
19
│ ├── config.py
│ ├── drivers.py
│ ├── entities
│ │ ├── __init__.py ← Magic
│ │ ├── places.py
│ │ ├── restaurants.py
│ │ └── reviews.py
│ │
│ ├── exceptions.py
│ ├── facades.py
│ ├── main.py
│ └── storages.py
│
├── .gitignore
├── pyproject.toml
└── README.md
The imports now can become simple again. You’re welcome!
from gmaps_crawler.entities import Place, Restaurant, Review # __init__.py magic
Managing dependencies
You’re going to end up having to install some libraries regardless of the project.
I can mention some like rich , pytest , boto3 , django , fastapi , tryceratops , mypy
, etc.
We use frameworks and tools to speed up our environment.
Let’s go straight to the point. requirements.txt is not good enough for your daily needs.
Here’s the process that some people follow:
1. pip install <lib> (Responsible for actually installing)
2. pip freeze > requirements.txt (Responsible for capturing installed versions and outputting
them to a file)
3. pip install -r requirements.txt (Installing all captured packages)
Over time it becomes a nightmare because:
• You have no clarity about what’s an intended dependency and what’s added as “sub depen-
dency”
• You need to manually update the requirements.txt (and remember your team to do the
same)
• If uninstalling you must do the same
• You will end up packaging everything for production since you don’t know which packages are
intended for dev usage and which for production usage
This file grows beyond the comprehension of anyone.
20
Example of requirements.txt Here’s an example. I’ll be installing a few libs and showing how
requirements.txt starts simple and grows.
After installing Rich
❯ pip install rich
...
❯ pip freeze
commonmark==0.9.1
Pygments==2.12.0
rich==12.4.4
❯ pip freeze
attrs==21.4.0
commonmark==0.9.1 # ← From Rich
iniconfig==1.1.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
Pygments==2.12.0 # ← From Rich
pyparsing==3.0.9
pytest==7.1.2
rich==12.4.4 # ← From Rich
tomli==2.0.1
❯ pip freeze
attrs==21.4.0
boto3==1.24.25
botocore==1.27.25
commonmark==0.9.1
iniconfig==1.1.1
jmespath==1.0.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
Pygments==2.12.0
pyparsing==3.0.9
pytest==7.1.2
python-dateutil==2.8.2
rich==12.4.4
s3transfer==0.6.0
six==1.16.0
tomli==2.0.1
urllib3==1.26.10
21
Poetry: the red pill
Take the red pill and stay with me. We’re going to add the same libs and see how our project metadata
grows.
You still need to manually install poetry though, let’s do that: pip install poetry
And now do the following:
❯ poetry init
This command will guide you through creating your pyproject.toml config.
22
- A file path (../my-package/my-package.whl)
- A directory (../my-package/)
- A url (https://example.com/packages/my-package-0.1.0.tar.gz)
Add a package:
Would you like to define your development dependencies interactively? (yes/no) [yes]
Search for package to add (or leave blank to continue): rich
Found 20 packages matching rich
23
[9] pytest-faker
> 0
Enter the version constraint to require (or leave blank to use the latest version):
Using version ^7.1.2 for pytest
Add a package:
Generated file
[tool.poetry]
name = "python-like-pro"
version = "0.1.0"
description = "I will never use requirements.txt again"
authors = ["Guilherme Latrova <hello@guilatrova.dev>"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.10"
boto3 = "^1.24.25"
[tool.poetry.dev-dependencies]
rich = "^12.4.4"
pytest = "^7.1.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Note you just generated an initial pyproject.toml file. You didn’t install anything yet.
Let’s see what’s inside first!
[tool.poetry]
name = "python-like-pro"
version = "0.1.0"
description = "I will never use requirements.txt again"
authors = ["Guilherme Latrova <hello@guilatrova.dev>"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.10"
boto3 = "^1.24.25"
[tool.poetry.dev-dependencies]
rich = "^12.4.4"
pytest = "^7.1.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
24
Run poetry install when you feel ready, and you’re going to notice a new generated file named:
poetry.lock .
This file holds all the boring and hard to understand stuff.
Let’s point out some relevant stuff from the first section:
[[package]]
name = "atomicwrites" # <-- ← Package name
version = "1.4.0" # <-- ← Version used name
description = "Atomic file writes."
category = "dev" # <-- ← Is it going to prod or be in dev?
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
The best part is that you don’t have to deal with it ever!
Adding new dependencies Before adding anything new you must ask yourself: Do my final
user need it?
I bet your user won’t need pytest and linters. Thus these would be dev dependencies.
Anything else required for your software to be executed becomes a production dependency.
Add production dependencies
Whenever you want to add a new dependency intended to go to production, you run:
poetry add <lib> , for example: poetry add fastapi
Note how either pyproject.toml and your package.lock gets updated.
Add dev dependencies
Whenever you want to add a new dependency intended to be used locally across the team, you run:
poetry add -D <lib> , for example: poetry add -D tryceratops
pyproject.toml vs requirements.txt
25
This is annoying. Don’t do that.
Poetry allows you to install your package locally so you can import without the src prefix.
You just need to add packages under tool.poetry :
[tool.poetry]
name = ">>> YOUR PROJECT NAME"
version = "0.1.0"
authors = ["Guilherme Latrova <your@email.com>"]
description = ">>> YOUR PROJECT DESCRIPTION"
license = ">>> LICENSE"
packages = [
{ include = ">>> YOUR PACKAGE NAME", from = "src" }, # ← This is the trick
]
So whenever you or your team runs poetry install it will automatically handle it for you.
For simpler scripts I recommend either: bin or a scripts directory at the root.
Be my guest to come up with other names. As long as it works for you and your team that’s enough.
It would introduce the following directory:
gmaps_crawler
├── bin ← New directory
│ ├── setup.sh ← Potential command to setup project
│ └── linters.sh ← Command to run all linters in the project
│
├── src
│ └── gmaps_crawler
│ ├── __init__.py
│ ├── config.py
│ ├── drivers.py
│ ├── entities.py
│ ├── exceptions.py
│ ├── facades.py
│ ├── main.py
│ └── storages.py
│
26
├── .gitignore
├── pyproject.toml
└── README.md
The above recommendation won’t work well in case you need to write longer/complex scripts or even
reuse Python code to generate whatever you need.
Like when you want to reuse your ORM to store data into the database, or even reuse some
Enum/Constant you already defined in code.
If that’s your case you should ditch your simple scripts dir and use something more robust like
pyinvoke .
When using Pyinvoke you need to create a tasks.py file at the root:
gmaps_crawler
├── src
│ └── gmaps_crawler
│ ├── __init__.py
│ ├── config.py
│ ├── drivers.py
│ ├── entities.py
│ ├── exceptions.py
│ ├── facades.py
│ ├── main.py
│ └── storages.py
│
├── tasks.py ← Required for Pyinvoke
├── .gitignore
├── pyproject.toml
└── README.md
27
for pattern in patterns:
c.run("rm -rf {}".format(pattern))
Note: Pyinvoke is not a replacement for Typer or click Some people end up thinking Pyinvoke
is a CLI lib. It’s not.
28
That’s not its purpose! (Even though it looks like a regular CLI).
Think of Pyinvoke as a development’s command cheat sheet that is kept local and Typer/click as the
user interface that is deployed to the end user.
Use Linters
Have you ever tried asking your teammates to don’t write long lines and to clear up imports when
they’re not being used?
That’s easier said than done.
We can’t blame them. When focused on working, it’s hard to remember of amenities that keep
your code clean. That’s where linters help to warn the team and ensure we’re following the same
standards across the company.
Linters are tools that analyze your work, complain about inconsistencies, and suggest changes. Al-
most like our ex-girlfriends but focused on code.
The sooner you apply this to any project the better and the more consistent it will be.
Linter recommendations
You’re about to see a few recommendations. I’ve used them successfully in many past companies
and they worked fine to get us in sync allowing us to focus on what matters: producing code.
Linter Reasoning
black Formatting
flake8 General linting (e.g. unused variables)
isort Sort of imports
mypy Typing validation
tryceratops Avoid try/except anti-patterns
end-of-file-fixer / trailing-whitespace General clean up
We’re going to discuss the importance of type hinting (mypy) and good practices with try/except
(tryceratops) over the next chapters.
Recommended settings
The importance of cleaning your code Let’s face it. The more code you add the harder it is to
maintain.
We should strive for less code and more value. That’s efficiency. Doing more with less.
Linters like flake8 keep your code clear by removing unused code (imports, vars, etc).
The importance of formatting You might not agree with everything Black suggests (check out
Blue for a different take), but it’s undeniable that getting your team following the same set of rules
increases productivity.
I’ve seen many people adding blank lines and different spacing, only to later, someone else removes
them .
The goal of using a formatting linter is to agree on something without thinking.
To focus on the code and ignore the whitespaces and commas.
To not have to measure whether your line is too long.
If not black, then try blue, or anything else, just have your team agree on something.
29
Formatting: Line length PEP 8 suggests 79 characters line length while Black enforces 88 char-
acters by default.
Chances are your current monitor is bigger, so why restrain ourselves?
It gets even harder to respect <=88 length when we start adding type hints. See how easy it is to
go over the recommendation:
# ••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• (Black limit) ↓
# •••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• (PEP 8 limit) ↓
class OrderService:
def handle_client_order(self, client: Client, purchased_items: list[Purchases]) -> Order:
...
It led me and my colleagues to play with other numbers. We tried both 100 and 120. 120 is GREAT!
Use it.
Oh, and don’t be afraid of testing other numbers for your project. Don’t restrain yourself because of
PEP 8, Black, or myself. We’re all humans with opinions based on our experiences and so are you.
Start with a recommendation and formulate your own opinion!
Imports: Why sorting It looks good to the eyes to see imports grouped as standard lib, third-party,
and local packages, and sorted alphabetically.
You can find yourself faster as you navigate code and import stuff.
Set up pyproject.toml We can also use pyproject.toml to set up our settings (line length, and
etc).
Here’s what I recommend:
[tool.black]
line-length = 120 # ← Override default line length
[tool.isort]
profile = "black" # ← Matches internal configs with Black, so they don't fight each other
line_length = 120 # ← Required to sync with Black
# ↓ I always feel like pytest and pydantic are core libs (they aren't),
# ↓ so it feels natural to me to organize them together with os, unittest, etc
extra_standard_library = ["pytest", "pydantic"]
[tool.tryceratops]
ignore = ["TC002", "TC003"]
[tool.mypy]
python_version = "3.10"
warn_unused_configs = true
warn_unused_ignores = true
namespace_packages = true
explicit_package_bases = true
ignore_missing_imports = true
plugins = [
"pydantic.mypy"
]
Unfortunately, at the time of this writing flake8 doesn’t support pyproject.toml and their main-
tainers have no intentions of supporting it (yes, they’re opinionated as well!). So you need to create
a .flake8 file:
30
[flake8]
max-line-length=120 # ← Required to sync with Black/isort
extend-ignore=E203 # ← Required to sync with Black
exclude=__init__.py # ← Frequently I found myself willing to ignore __init__.py files
Enforce linters
Ensure the versions set in pre-commit are in sync with the versions set in pyproject.toml .
31
This needs to be done manually.
Once done you can execute it manually by:
pre-commit run -a
or commit any file and it will automatically run.
Guarantee linters are being followed No, you can’t trust your beautiful linters will be respected.
For a variety of reasons people disable them, forget to set them up, or simply don’t care.
You must use your Continuous Integration (aka CI) system to ensure it’s respected.
If you use GitHub, you can enforce your pre-commit rules are respected by using an action:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Lint
uses: pre-commit/action@v2.0.3 # ← Here
32
├── pyproject.toml
├── README.md
├── scripts
│ ├── bootstrap.sh
│ ├── code-coverage.sh
│ ├── fixlinters.sh
│ ├── integration-test.sh
│ ├── linters.sh
│ ├── makemigrations.sh
│ ├── manage.sh
│ ├── migrate.sh
│ ├── payloads
│ │ └── task_sample.json
│ ├── produce_task.sh
│ ├── setup-env.sh
│ ├── shell.sh
│ ├── unit-test.sh
│ └── watch-test.sh
│
├── sonar-project.properties
└── src
├── consumer.py
├── __init__.py
├── manage.py
├── onfleet_adapter
│ ├── asgi.py
│ ├── __init__.py
│ ├── models.py
│ ├── settings
│ │ ├── base.py
│ │ ├── development.py
│ │ ├── __init__.py
│ │ ├── local.py
│ │ ├── production.py
│ │ └── staging.py
│ │
│ ├── urls.py
│ ├── views.py
33
│ └── wsgi.py
│
├── onfleet_tasks
│ ├── apps.py
│ ├── dtos.py
│ ├── exceptions
│ │ ├── base.py
│ │ ├── db.py
│ │ ├── general.py
│ │ ├── __init__.py
│ │ ├── interruption.py
│ │ └── state.py
│ ├── facades.py
│ ├── factories
│ │ ├── events.py
│ │ ├── __init__.py
│ │ └── payloads.py
│ ├── __init__.py
│ ├── listeners
│ │ ├── agents.py
│ │ ├── __init__.py
│ │ └── topics.py
│ ├── managers.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_featureflag.py
│ │ ├── 0003_auto_20201123_1925.py
│ │ ├── enums
│ │ │ ├── task_status
│ │ │ │ └── 0001.sql
│ │ │ └── task_type
│ │ │ └── 0001.sql
│ │ └── __init__.py
│ ├── models
│ │ ├── enums.py
│ │ ├── feature_flags.py
│ │ ├── __init__.py
│ │ └── tasks.py
34
│ ├── serializers.py
│ ├── services
│ │ ├── brokers.py
│ │ ├── __init__.py
│ │ ├── onfleet.py
│ │ └── webhooks.py
│ ├── urls.py
│ ├── utils.py
│ └── views.py
│
└── tests
├── conftest.py
├── fixtures
│ ├── address_incidents.py
│ ├── __init__.py
│ ├── onfleet.py
│ └── tasks.py
├── __init__.py
├── integration
│ ├── conftest.py
│ ├── __init__.py
│ ├── onfleet_tasks
│ │ ├── __init__.py
│ │ ├── services
│ │ │ ├── __init__.py
│ │ │ └── test_onfleet.py
│ │ └── test_views.py
│ └── test_onfleet_setup.py
├── unit
│ ├── __init__.py
│ └── onfleet_tasks
│ ├── exceptions
│ │ ├── __init__.py
│ │ ├── test_db.py
│ │ ├── test_general.py
│ │ ├── test_interruption.py
│ │ └── test_state.py
│ ├── factories
35
│ │ ├── __init__.py
│ │ ├── test_events.py
│ │ └── test_payloads.py
│ ├── __init__.py
│ ├── listeners
│ │ ├── __init__.py
│ │ └── test_agents.py
│ ├── services
│ │ ├── __init__.py
│ │ ├── test_onfleet.py
│ │ └── test_webhooks.py
│ ├── test_dtos.py
│ ├── test_facades.py
│ ├── test_serializers.py
│ └── test_urls.py
│
└── utils
├── dates.py
├── factories
│ └── onfleet_tasks.py
├── __init__.py
├── onfleet.py
├── random.py
├── serializers.py
└── urls.py
Template Link
Regular https://github.com/guilatrova/python-template
project
Lambda https://github.com/guilatrova/sam-lambda-python-template
with SAM
36