<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gordon Myers</title>
    <description>The latest articles on DEV Community by Gordon Myers (@soapergem).</description>
    <link>https://dev.to/soapergem</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F601320%2Ffa2a44f3-56d1-4757-af98-b8d60a96f418.png</url>
      <title>DEV Community: Gordon Myers</title>
      <link>https://dev.to/soapergem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/soapergem"/>
    <language>en</language>
    <item>
      <title>What I wish I knew about Python when I started</title>
      <dc:creator>Gordon Myers</dc:creator>
      <pubDate>Tue, 11 Mar 2025 15:09:17 +0000</pubDate>
      <link>https://dev.to/soapergem/what-i-wish-i-knew-about-python-when-i-started-14eh</link>
      <guid>https://dev.to/soapergem/what-i-wish-i-knew-about-python-when-i-started-14eh</guid>
      <description>&lt;p&gt;Seven years ago, I quit my job working for a startup and joined a new company that did all their development in Python. I had never used the Python language to any serious degree up until that point, but quickly learned to love the language for its intuitive nature and broad community support. At the same time, it often felt a bit like the wild west as there seemed to exist at least ten different ways to accomplish any one task, and no obvious consensus on which one was right.&lt;/p&gt;

&lt;p&gt;Since then, through a combination of learning best practices from peers and colleagues and gaining firsthand experience through trial and error, I've developed a set of choices I now make—ones I only wish I had known about back then. Some of these ideas didn't even exist seven years ago (like &lt;a href="https://peps.python.org/pep-0621/" rel="noopener noreferrer"&gt;PEP-621&lt;/a&gt;), so implementing them back then would have been impossible. But now, I'm sharing them here for anyone in a similar position. Keep in mind that the Python ecosystem evolves quickly, so it's entirely possible much of the advice here may become obsolete within a year. And of course, you may not agree with all my recommendations—and that's okay! Feel free to debate (or roast) me in the comments.&lt;/p&gt;

&lt;p&gt;I'll be breaking this down into seven sections, so let's dive in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Folder structure and basics
&lt;/li&gt;
&lt;li&gt;  Virtual environments and dependency management
&lt;/li&gt;
&lt;li&gt;  Publishing packages
&lt;/li&gt;
&lt;li&gt;  Logging
&lt;/li&gt;
&lt;li&gt;  Finding open-source software packages
&lt;/li&gt;
&lt;li&gt;  Code formatting
&lt;/li&gt;
&lt;li&gt;  Testing and debugging
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Folder structure and basics
&lt;/h2&gt;

&lt;p&gt;The first thing I want to bring up probably seems small and inconsequential, but it was definitely something that tripped me up a few times when I first started. When starting a new project, I would naturally create a folder to store all my files, something like &lt;code&gt;myproject&lt;/code&gt;. This folder would invariably be the root of a Git repository. I recommend avoiding storing Python files directly in the root of this folder. Instead, create a subfolder within &lt;code&gt;myproject&lt;/code&gt; also named &lt;code&gt;myproject&lt;/code&gt; and store your Python code there.&lt;/p&gt;

&lt;p&gt;Alternatively, you might create a subfolder called &lt;code&gt;src&lt;/code&gt; for the code, but if you plan on using any of your new Python files as packages, you'll probably still need another subfolder inside &lt;code&gt;src&lt;/code&gt; called &lt;code&gt;myproject&lt;/code&gt;. I will still include other files at the root of the repository—just not Python files. Here are some examples of other files that might live at the root:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;Dockerfile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Justfile&lt;/code&gt; / &lt;code&gt;Makefile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;pyproject.toml&lt;/code&gt; (more on this later)&lt;/li&gt;
&lt;li&gt;  Other config files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now keep in mind that rules are made to be broken, so there are exceptions to this. My baseline rule is to always avoid placing Python files directly at the root of a new repository when setting it up. Why, you might ask? Because the folder name of your repository will always be effectively invisible to the Python importer—it needs a subfolder in order to import things by name. So, suppose I have two files in my repository: an executable script called &lt;code&gt;main.py&lt;/code&gt; and a utility class called &lt;code&gt;utils.py&lt;/code&gt; from which I want to import a function. If I threw everything in the root of the repository, the folder structure would look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├── main.py
├── utils.py
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and then my main function would look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myproject.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;my_function&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;my_function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, then it simply wouldn't work, would it? Although the root &lt;code&gt;myproject&lt;/code&gt; folder is indeed part of the &lt;code&gt;PYTHONPATH&lt;/code&gt; by running it inside that folder, the package name &lt;code&gt;myproject&lt;/code&gt; still isn't available unless—and until—we have that extra nested folder, so it should look more like this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├── README.md
└── myproject
    ├── main.py
    └── utils.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, I recognize this probably seems small and inconsequential, but coming from a non-Pythonic background, I remember it felt weird at first. Just accept that this is the convention. And while we're talking about basics, there are a few other things worth mentioning. You may have noticed that the first line of my sample Python script above included a shebang. It's standard practice to include this only in files meant to be executed directly. Furthermore, I used &lt;code&gt;/usr/bin/env python&lt;/code&gt; in my shebang, but in the wild you might come across &lt;code&gt;/usr/bin/python&lt;/code&gt; or &lt;code&gt;/usr/bin/local/python&lt;/code&gt;. I recommend sticking with &lt;code&gt;/usr/bin/env python&lt;/code&gt;, as it's the preferred approach outlined in &lt;a href="https://peps.python.org/pep-0394/#for-python-script-publishers" rel="noopener noreferrer"&gt;PEP-394&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, whatever file launches your application (in this case, mine is &lt;code&gt;main.py&lt;/code&gt;), it is conventional to include that &lt;code&gt;if __name__ == "main":&lt;/code&gt; block at the bottom (shown above), to specify the entrypoint to your application. This isn't strictly necessary in many use cases—for instance, if you're writing a REST API using &lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt; and launching it via &lt;a href="https://www.uvicorn.org/" rel="noopener noreferrer"&gt;uvicorn&lt;/a&gt;, you don't need this block. But it also doesn't hurt to have it regardless. It's a helpful reminder of where things begin.&lt;/p&gt;

&lt;p&gt;One critical piece of learning I want to share is that, when creating files, do your best not to name them with the same names as Python system packages. If you do, it will invetiably cause import issues. I remember burning more time than I would care to admit, scratching my head over why one of my scripts wasn't running, all because I had created a file named &lt;code&gt;copy.py&lt;/code&gt; or &lt;code&gt;email.py&lt;/code&gt; (which conflict with the &lt;a href="https://docs.python.org/3/library/copy.html" rel="noopener noreferrer"&gt;copy&lt;/a&gt; and &lt;a href="https://docs.python.org/3/library/email.html" rel="noopener noreferrer"&gt;email&lt;/a&gt; system packages, respectively). So, watch out for that!&lt;/p&gt;

&lt;p&gt;Additionally, I sometimes create repositories that contain more than just Python code, in which case I further segment the folder structure. This goes beyond simply including template files alongside my Python code—I mean entirely separate projects within the same repository. This approach is often referred to as building a monorepo, where a single repository houses multiple projects. The most common structure I use is really more of a pseudo-monorepo containing two different projects: a backend and a frontend application. In many cases, my frontend application doesn't include any Python at all, as JavaScript frameworks often make more sense. In such cases, my folder structure might look more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├── README.md
├── backend
│   ├── myproject
│   │   ├── main.py
│   │   └── util.py
│   └── pyproject.toml
└── frontend
    └── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes when writing applications in Python (or any language, really), you may find yourself writing common utility functions that aren't specific to any one application. These might include functions for database connections, common data transformations, or reusable subclasses for workflow orchestration. In such cases, I recommend utilizing the earlier folder structure, where I create a folder called &lt;code&gt;myproject&lt;/code&gt; at the root of the repository. To keep things organized, I also recommend making use of the &lt;code&gt;__all__&lt;/code&gt; dunder variable to cleanly define what utilities are exported.&lt;/p&gt;

&lt;p&gt;To demonstrate this, I created a sample repository called &lt;a href="https://github.com/soapergem/rds-utils" rel="noopener noreferrer"&gt;rds-utils&lt;/a&gt;, which I reference later when we discuss publishing packages. If you peruse that repository, you'll see how I have things structured for this library. Note the package itself is called &lt;code&gt;rds-utils&lt;/code&gt; with a hyphen, but the root-level project folder is named &lt;code&gt;rds_utils&lt;/code&gt; with an underscore. This is intentional. When writing import statements in Python, you cannot use hyphens, though it is relatively common to see hyphens used in the package names themselves. So with this repo, you would run &lt;code&gt;pip install rds-utils&lt;/code&gt; but then use something like &lt;code&gt;from rds_utils import fetch_query&lt;/code&gt; in your scripts.&lt;/p&gt;

&lt;p&gt;I also use the &lt;code&gt;__all__&lt;/code&gt; special variable in my &lt;a href="https://github.com/soapergem/rds-utils/blob/main/rds_utils/__init__.py" rel="noopener noreferrer"&gt;__init__.py file&lt;/a&gt;. Generally, I prefer to leave most of my __init__.py files completely blank (or omit them entirely), but defining the &lt;code&gt;__all__&lt;/code&gt; variable within them is my one exception. This essentially allows you to create a cleaner interface when building shared libraries so that consumers of your library don't have to worry about internal package structures. The way I have it set up right now, it's still possible to directly import things like: &lt;code&gt;from rds_utils.utils import fetch_query&lt;/code&gt;, but it's much nicer that consumers don't need to know about that extra &lt;code&gt;.utils&lt;/code&gt; that I have defined there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Virtual environments and dependency management
&lt;/h2&gt;

&lt;p&gt;In the seven years I've been working with Python, the landscape has changed dramatically. It feels especially true now, as the Python community reaps the benefits of what you might call the "Rust Renaissance." So although it still seems very nascent, I want to focus on a tool called &lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;uv&lt;/a&gt; which I've recently adopted and has streamlined my workflow by replacing three other tools I previously relied on. While uv helps manage virtual environments, it does a whole lot more. It also helps manage Python versions as well as project dependencies and can even be used to create packages.&lt;/p&gt;

&lt;p&gt;When I first started writing this post, I had planned on getting into the nuances of pyenv, pdm, pipx—all of which I had been using. Just last year, I thought &lt;a href="https://dev.to/bowmanjd/python-tools-for-managing-virtual-environments-3bko"&gt;this blog post&lt;/a&gt; comparing Python package managers was still an insightful guide, but now it already seems outdated! While tools like pyenv and pdm still have advantages over uv in certain areas, those differences are minor, and I'm increasingly confident that uv will iron out any of my grievances as they continue to move forward with development.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7i92idob76cdvaya03pt.png" alt="uv" width="100" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But let me back up and start from the perspective of a total Python beginner, as that is who this post is intended for. In Python, there are a lot of built-in libraries available to you via the &lt;a href="https://docs.python.org/3/library/index.html" rel="noopener noreferrer"&gt;Python Standard Library&lt;/a&gt;. This includes packages like &lt;code&gt;datetime&lt;/code&gt; which allows you to manipulate dates and times, or like &lt;code&gt;smtplib&lt;/code&gt; which allows you to send emails, or like &lt;code&gt;argparse&lt;/code&gt; which helps aid development of command line utilities, and so on. In addition, Python also has third-party libraries available through &lt;a href="https://pypi.org/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;, the Python package index. You don't need to do anything special to utilize any of the Python Standard Library packages besides using an &lt;code&gt;import&lt;/code&gt; statement in your Python script to reference them. But with PyPI packages you typically use &lt;code&gt;pip install&lt;/code&gt; to first add them into your system, then reference them with the &lt;code&gt;import&lt;/code&gt; keyword.&lt;/p&gt;

&lt;p&gt;Pip is a command line package manager that comes with Python, but it is definitely not the only package manager. Other tools like pipenv, poetry, pdm, conda, and even some third-party applications like &lt;a href="https://please.build" rel="noopener noreferrer"&gt;please&lt;/a&gt; can all manage Python dependencies. One common mistake beginners make is installing Python, and then running &lt;code&gt;pip install&lt;/code&gt; (or even worse, &lt;code&gt;sudo pip install&lt;/code&gt;), before moving forward. While this works in the moment, it will almost certainly cause headaches for you later.&lt;/p&gt;

&lt;p&gt;In Python, you cannot have multiple versions of the same package installed at the same time. A problem arises when one piece of Python code depends upon, say, version 1 of Pydantic, while another depends on version 2. Or maybe one package depends on an older version of scikit-learn, but another needs the newer version, and so on—these are called dependency conflicts. To solve this problem, Python has a concept called virtual environments. These are simply an isolated context in which you can install a specific set of Python packages, unique to one application. So, if you have code in a folder named &lt;code&gt;myproject1&lt;/code&gt; and some other code in a folder called &lt;code&gt;myproject2&lt;/code&gt;, you can have separate virtual environments for each.&lt;/p&gt;

&lt;p&gt;It's for this reason, I recommend never installing any Python packages into your global environment at all. Only ever install into a virtual environment. Prior to recent updates to uv, I would have recommended installing pyenv first as a way of managing different versions of Python, and then using its built-in extension pyenv-virtualenv to manage your virtual environment. However, as I teased earlier, uv now handles all of this. With that foundation in place, let's see why uv is my go-to tool.&lt;/p&gt;

&lt;p&gt;To begin, &lt;strong&gt;I recommend installing uv first&lt;/strong&gt;, even before installing Python, via this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once uv is installed, you can use it to install the latest version of Python, via this command (as of version 3.13):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv python &lt;span class="nb"&gt;install &lt;/span&gt;3.13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That will download the specified version of Python to a subfolder under &lt;code&gt;~/.local/share/uv/python&lt;/code&gt; but it won't be immediately available. You can now type &lt;code&gt;uv run python&lt;/code&gt; to launch this instance of Python, or &lt;code&gt;uv run --python 3.13 python&lt;/code&gt; if you want to specify the version. Lately, I've been adding the following aliases to my &lt;code&gt;.bashrc&lt;/code&gt; or &lt;code&gt;.zshrc&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;python&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"uv run python"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;pip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"uv pip"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows me to simply type &lt;code&gt;python&lt;/code&gt; on the command line, and it effectively calls that same &lt;code&gt;uv run python&lt;/code&gt; command to launch the Python REPL. Or I can type &lt;code&gt;pip list&lt;/code&gt;, and it will call &lt;code&gt;uv pip list&lt;/code&gt; instead. So at this point, we have uv managing our different versions of Python (because inevitably you will need to update both), but we are still operating within the global environment for that version of Python. As I mentioned before, we want to utilize virtual environments as a means of segregating dependencies for different projects.&lt;/p&gt;

&lt;p&gt;Even when using uv, there are multiple ways of going about this. You could type &lt;code&gt;uv venv&lt;/code&gt; which will create a folder named &lt;code&gt;.venv&lt;/code&gt;, then run &lt;code&gt;source .venv/bin/activate&lt;/code&gt; to activate it, and then run &lt;code&gt;uv pip install&lt;/code&gt; (or just &lt;code&gt;pip install&lt;/code&gt; with the alias I mentioned earlier) to install dependencies into it. You could do that, but that's not what I recommend. If you're brand new to virtual environments, you might not realize they are something you have to "activate" and "deactivate." But before I suggest a better alternative, I need to introduce one more concept: Python packaging.&lt;/p&gt;

&lt;p&gt;Virtual environments solve one problem: conflicting dependencies across different projects. But they don't solve an accompanying problem, which is what to do when you've created a project and need to package it to share with others. Virtual environments live on your computer and provide an isolated space to install dependencies, but if you want to publish your project to the Python Package Index, or if you want to install the project inside a Docker container, a separate process is involved as virtual environments aren't portable.&lt;/p&gt;

&lt;p&gt;Different tools that have sprung up over the years to solve this problem, some of which I already mentioned. Pip uses a requirements.txt with a list of each dependency on every line. The related pip-tools package uses a requirements.in file, which barely abstracts that a little further. Pipenv uses a TOML-formatted file named Pipfile to define much of the same, while Anaconda and Conda use an environment.yml file. The setuptools package defines a Python script named setup.py to handle packaging and define dependencies. And tools like poetry and pdm use a pyproject.toml file.&lt;/p&gt;

&lt;p&gt;That last one—pyproject.toml—was introduced in mid-2020 as part of &lt;a href="https://peps.python.org/pep-0621/" rel="noopener noreferrer"&gt;PEP-621&lt;/a&gt;, and in my opinion is the best way to handle Python packaging to date. All the others are antiquated or incomplete by comparison, and fortunately, pyproject.toml is uv's native format. Here's an example of a pyproject.toml file generated by uv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-email-application"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Sends emails using SES"&lt;/span&gt;
&lt;span class="py"&gt;readme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"README.md"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.13&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"boto3&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.35&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"click&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;8.1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"tqdm&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.67&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, it has a name, a version, a description, a reference to a README.md file, the specific Python version required, and a list of dependencies. I generated this file by first creating a project folder named &lt;code&gt;my-email-application&lt;/code&gt; and then running the following commands from within that folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv init
uv add boto3 click tqdm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In truth, I did have to manually update the pyproject.toml, which was generated by that first &lt;code&gt;uv init&lt;/code&gt; command, because it had placeholder text in the description. But by calling the second command (&lt;code&gt;uv add&lt;/code&gt;), not only did it update the pyproject.toml file as shown above with the list of dependencies, it created a &lt;code&gt;.venv&lt;/code&gt; folder and installed those dependencies into it. So now if I type &lt;code&gt;uv pip list&lt;/code&gt;, I immediately see the following output:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Package         Version
--------------- -----------
boto3           1.35.68
botocore        1.35.68
click           8.1.7
jmespath        1.0.1
python-dateutil 2.9.0.post0
s3transfer      0.10.4
six             1.16.0
tqdm            4.67.1
urllib3         2.2.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Because these are installed into a virtual environment, I can trust they won't conflict with any other projects I might be working on. My recommendation is to always start a project by running &lt;code&gt;uv init&lt;/code&gt;, which will create a pyproject.toml file (if one does not already exist). However, if you happen to be working on a project that already has this file, you'll need to call the following two commands in order instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv lock
uv &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first will use the existing pyproject.toml file to create another file called uv.lock, while the second will create a virtual environment and install all the dependencies into it.&lt;/p&gt;

&lt;p&gt;Further, I recommend only installing dependencies using &lt;code&gt;uv add&lt;/code&gt;, instead of something like &lt;code&gt;uv pip install&lt;/code&gt;. Using &lt;code&gt;uv add&lt;/code&gt; will not only install dependencies into your virtual environment, but will also update your pyproject.toml and uv.lock files. This means you don't have to treat dependency installation and packaging as two separate things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Python on Windows
&lt;/h3&gt;

&lt;p&gt;If you are running Microsoft Windows, I want to advise one more prerequisite step that you need to take before getting started with Python or uv: install the &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/about" rel="noopener noreferrer"&gt;Windows Subsystem for Linux&lt;/a&gt;, also known as WSL2. &lt;strong&gt;Do not&lt;/strong&gt;, for the love of all that is good and holy, install Python tooling directly in Windows; rather, &lt;strong&gt;install WSL first&lt;/strong&gt;. &lt;a href="https://gcore.com/learning/how-to-install-wsl-2-on-windows/" rel="noopener noreferrer"&gt;This guide&lt;/a&gt; outlines all the steps you need to take to get started, though I recommend downloading WSL from the &lt;a href="https://github.com/microsoft/WSL/releases" rel="noopener noreferrer"&gt;Releases page on Github&lt;/a&gt; instead of from the Microsoft Store as advised in Step 3.&lt;/p&gt;

&lt;p&gt;WSL transforms the Windows command prompt into a Linux terminal, along with its own Linux-based filesystem in a cordoned off part of your hard drive. The files there are still accessible through programs like VS Code, via their WSL extension. Installing WSL does mean you will need to learn Linux syntax, but it will be worth it. So for Windows users, install WSL first, &lt;em&gt;then&lt;/em&gt; install uv.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on tools
&lt;/h3&gt;

&lt;p&gt;It's also worth mentioning that there are some utility packages offering helpful tools that I like to make globally available. This approach should be used sparingly—since most of the time you will want to install project dependencies as I've described above using &lt;code&gt;uv add&lt;/code&gt;—there are certain tools that cover cross-cutting concerns and don't properly belong to any one project you're writing.&lt;/p&gt;

&lt;p&gt;An example of a cross-cutting concern would be something like code formatting, or CLI utilities helpful during development but not properly part of the codebase itself. There are only four tools I use in particular, but I'll list some of the more popular examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  black - a Python code formatter&lt;/li&gt;
&lt;li&gt;  flake8 - a Python code formatter&lt;/li&gt;
&lt;li&gt;  lefthook - a Git hooks manager&lt;/li&gt;
&lt;li&gt;  isort - a utility for sorting import statements&lt;/li&gt;
&lt;li&gt;  mypy - a static typing utility for Python&lt;/li&gt;
&lt;li&gt;  nbstripout - a utility that strips output from Jupyter notebooks&lt;/li&gt;
&lt;li&gt;  pdm - another package manager&lt;/li&gt;
&lt;li&gt;  poetry - another package manager&lt;/li&gt;
&lt;li&gt;  pre-commit - a Git hooks manager&lt;/li&gt;
&lt;li&gt;  pylint - a static code checker for Python&lt;/li&gt;
&lt;li&gt;  ruff - a Python code formatter&lt;/li&gt;
&lt;li&gt;  rust-just - a modern command runner, similar to Make&lt;/li&gt;
&lt;li&gt;  uv-sort - a utility for alphabetizing dependencies in pyproject.toml&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These days, I only keep lefthook, mypy, ruff, and rust-just installed. I've stopped using pdm in favor of uv, and I've stopped using tools like black and isort in favor of ruff. In the past, I might have installed these utilities with a command like &lt;code&gt;pip install black&lt;/code&gt; or &lt;code&gt;pipx install pdm&lt;/code&gt;. But with uv, the equivalent command is &lt;code&gt;uv tool install&lt;/code&gt; followed by the name of the tool. This makes the exectuable available globally regardless of whether you're in a virtual environment. You can also run &lt;code&gt;uv tool upgrade&lt;/code&gt; periodically to make sure you're using the latest version.&lt;/p&gt;

&lt;p&gt;I use three criteria to determine whether to install a tool via &lt;code&gt;uv tool install&lt;/code&gt; or the usual &lt;code&gt;uv add&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The tool must primarily provide a binary application (not a library) &lt;/li&gt;
&lt;li&gt;  The tool must address a cross-cutting concern (such as code formatting)&lt;/li&gt;
&lt;li&gt;  The tool must not be something you would otherwise bundle with an application itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that said, there is still one tool that covers cross-cutting concerns, and I recommend packaging the normal way with the application: pytest. Pytest is used for running unit tests within your project, so it should be added as a development dependency. Add it with a command like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add &lt;span class="nt"&gt;--dev&lt;/span&gt; pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are other development dependencies you might consider adding (like coverage), but in general I tend to install things as tools rather than dev dependencies. It really depends on the use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing packages
&lt;/h2&gt;

&lt;p&gt;uv makes it remarkably easy to publish shared libraries and other utilities you've written in Python as packages—either on the public PyPI repository or in private artifact registries (e.g. Gitlab Artifacts, AWS CodeArtifact, Google Artifact Registry, Artifactory, SonaType Nexus, etc.). You only need to make minor modifications to your pyproject.toml file to support publishing, and then set some environment variables. I'll reference my rds-utils package I highlighted earlier to illustrate how this works. As you can see from &lt;a href="https://github.com/soapergem/rds-utils/blob/main/pyproject.toml" rel="noopener noreferrer"&gt;that project's prpyroject.toml file&lt;/a&gt;, I have added two blocks that aren't there by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;[project.urls]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;[tool.uv]&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The list of project.urls is not strictly required, but when publishing specifically to PyPI it means the website that PyPI auto-generates for your package will link back to your repository. I went ahead and published this library to PyPI &lt;a href="https://pypi.org/project/rds-utils/" rel="noopener noreferrer"&gt;here&lt;/a&gt;. You can see the left navigation bar lists the three "Project Links." The tool.uv section defines the publish URL, which in this case is PyPI's upload endpoint. If you're are using a private registry such as AWS CodeArtifact, you can swap that out here. For instance, a CodeArtifact repository URL might look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv]&lt;/span&gt;
&lt;span class="py"&gt;publish-url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://registry-name-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/artifacts/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Authentication for the upload endpoint is not defined here, nor should it be. Usernames, passwords, and/or tokens should never be hard-coded into a file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the case of a private registry, you may be given an actual username and password. In the case of the public PyPI registry, you need to generate an API token, and that token effectively becomes the password while the literal string &lt;code&gt;__token__&lt;/code&gt; is the username. To use uv for publishing, you need to set the values of these environment variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  UV_PUBLISH_USERNAME&lt;/li&gt;
&lt;li&gt;  UV_PUBLISH_PASSWORD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those values set, you then need to run the following two commands in order:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv build
uv publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And voila! Assuming you've specified the right credentials, you should now have a published Python package in your choice repository. In the past, this might have involved defining a setup.py file at the root of your repository, and using utility packages such as setuptools, wheel, and twine to build and upload everything, but now uv serves as a complete replacement for all of those.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing libraries from private repositories
&lt;/h3&gt;

&lt;p&gt;If you built your shared library package and published it to the public PyPI repository (as I did with my rds-utils package), you don't have to do anything special to utilize it in future projects—you can simply use &lt;code&gt;pip install rds-utils&lt;/code&gt; or &lt;code&gt;uv add rds-utils&lt;/code&gt;. But if you pushed your code to a private repository (common when developing commercial applications for private companies), you'll have to do a little bit more to tell uv (or pip) where to pull your package from and how to authenticate.&lt;/p&gt;

&lt;p&gt;I ran a quick experiment by creating a private CodeArtifact repository in my personal AWS account and installed my same rds-utils package using the steps described above. Then I created a second project in which I wanted to install that library, again using &lt;code&gt;uv init&lt;/code&gt; to create my pyproject.toml file. This time, I needed to manually add another section called &lt;code&gt;[[tool.uv.index]]&lt;/code&gt; to that file. In my case, it looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[tool.uv.index]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"codeartifact"&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://registry-name-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/artifacts/simple/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can add as many of these sections to your pyproject.toml file as you want, but it's unlikely you'll need more than one at a time. This is because a single private registry can host as many different Python packages as you like. Even Gitlab's project-based, built-in artifact registries still have a mechanism for pulling things at the group level, thereby allowing better consolidation.&lt;/p&gt;

&lt;p&gt;Again, &lt;strong&gt;it's critical to omit any authentication information from the URL&lt;/strong&gt;. With publishing to CodeArtifact, it's easy as the AWS credentials provided are already delineated into a separate URL, username, and password. But when fetching from CodeArtifact, AWS will present you with a message like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use &lt;code&gt;pip config&lt;/code&gt; to set the CodeArtifact registry URL and credentials. The following command will update the system-wide configuration file. To update the current environment configuration file only, replace global with site.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip config set global.index-url https://aws:$CODEARTIFACT_AUTH_TOKEN@registry-name-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/artifacts/simple/
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice that I've omitted the &lt;code&gt;aws:$CODEARTIFACT_AUTH_TOKEN@&lt;/code&gt; portion of the URL in my pyproject.toml entry. This is important, because uv already has its own mechanisms for supplying credentials to package indexes, which you should utilize instead. In this example, I can forego setting the CODEARTIFACT_AUTH_TOKEN environment variable and instead set the following two environment variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  UV_INDEX_CODEARTIFACT_USERNAME&lt;/li&gt;
&lt;li&gt;  UV_INDEX_CODEARTIFACT_PASSWORD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that the "CODEARTIFACT" portion of those environment variables is only that value because I happened to specify &lt;code&gt;name = "codeartifact"&lt;/code&gt; in the index definition in my pyproject.toml file. If I had set &lt;code&gt;name = "gitlab"&lt;/code&gt; then it would be &lt;code&gt;UV_INDEX_GITLAB_USERNAME&lt;/code&gt; instead. With CodeArtifact specifically, the username should be set to the literal string "aws" while the password should be the token value generated by the AWS CLI. With Gitlab, you would set the username to the literal string &lt;code&gt;__token__&lt;/code&gt; while the password would be an access token with the appropriate rights. Other registries will have different conventions, but hopefully you get the picture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Publishing Cloud Functions
&lt;/h3&gt;

&lt;p&gt;In addition to packaging shared libraries for PyPI and other registries, Python scripts are often deployed as Cloud Functions, such as AWS Lambda, Azure Functions, and Google Cloud Functions. After getting familiar with uv, I've learned it can be used to facilitate the packaging of cloud functions. The methods are a bit obscure, so I think it's worth explaining how to do it.&lt;/p&gt;

&lt;p&gt;In the case of Google Cloud Functions and Azure Functions, you need to deploy an actual requirements.txt file along with your Python script(s). You can utilize the following command to generate a requirements.txt file for this purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; requirements-txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I recommend using the &lt;code&gt;uv export&lt;/code&gt; command instead of &lt;code&gt;uv pip freeze&lt;/code&gt;, because the latter does not exclude dev dependencies. &lt;code&gt;uv export&lt;/code&gt; allows you to utilize the benefits of dev dependencies in your codebase while still ensuring what you package for deployment remains as slim as possible.&lt;/p&gt;

&lt;p&gt;With AWS Lambda, things are a little bit different. Google and Azure take care of installing the dependencies for you on the server side, but with AWS, it is your responsibility to install the dependencies ahead of time. Moreover, there are two ways of packaging Python scripts for Lambda execution: you can either upload a ZIP file containing your scripts and dependencies, or you can build a Docker image with a very specific Python package included and push it to ECR.&lt;/p&gt;

&lt;p&gt;In the former case, you can run commands like the following to build your deployable Lambda ZIP file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; dist &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;.egg-info&lt;span class="si"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;# cleanup&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.py dist  &lt;span class="c"&gt;# modify this if you have subfolders&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;dist
zip &lt;span class="nt"&gt;-r&lt;/span&gt; ../lambda.zip &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command will install any non-dev dependencies into the dist directory (you can name this whatever you want), then it will copy any Python files from the root directory of your repo into the same dist directory, and ZIP both the Python scripts and dependencies up into a deployable. Keep in mind that if you have created scripts inside subfolders, you might need to modify my line that copies .py files to the dist folder.&lt;/p&gt;

&lt;p&gt;Also note that when running that &lt;code&gt;uv pip install&lt;/code&gt; command, you may sometimes encounter errors like this one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Multiple top-level modules discovered in a flat layout.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are a couple of ways to cope with this error, but one simple way is to simply include the following block in your pyproject.toml file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.setuptools]&lt;/span&gt;
&lt;span class="py"&gt;py-modules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may encounter this error particularly if you've left Python scripts at the root of your repository, which often makes more sense when building Lambdas specifically. But if you follow my earlier tip to put all your code inside a subdirectory, this should also help you avoid seeing that error.&lt;/p&gt;

&lt;p&gt;The other way AWS Lambda can package Python code is with a Docker container. Lambda requires you include a special dependency called &lt;a href="https://pypi.org/project/awslambdaric/" rel="noopener noreferrer"&gt;awslambdaric&lt;/a&gt;, so when creating a Lambda function package with uv you'll need to add that as a dependency via &lt;code&gt;uv add awslambdaric&lt;/code&gt;. A Dockerfile for an AWS Lambda file might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ghcr.io/astral-sh/uv:python3.13-bookworm-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /opt&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; UV_CACHE_DIR=/tmp/.cache/uv&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; UV_PROJECT_ENVIRONMENT=/tmp/.venv&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml /opt&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; uv.lock /opt&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;uv venv
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /opt&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["uv", "run", "--no-dev", "python", "-m", "awslambdaric"]&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["lambda_function.handler"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical factors about this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  We have to set the &lt;code&gt;UV_CACHE_DIR&lt;/code&gt; environment variable to something under &lt;code&gt;/tmp&lt;/code&gt; because that's the only writable folder when Lambda is invoked&lt;/li&gt;
&lt;li&gt;  We also have to set the &lt;code&gt;UV_PROJECT_ENVIRONMENT&lt;/code&gt; to something under &lt;code&gt;/tmp&lt;/code&gt; for the same reason&lt;/li&gt;
&lt;li&gt;  We have to consistently use the &lt;code&gt;--no-dev&lt;/code&gt; flag with both sync and run&lt;/li&gt;
&lt;li&gt;  We copy the pyproject.toml and uv.lock files before calling &lt;code&gt;uv sync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  We copy the rest of the files after calling &lt;code&gt;uv sync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  We have to set the entrypoint to call the awslambdaric package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition, I make use of a &lt;code&gt;.dockerignore&lt;/code&gt; file (not shown here) so that the command to &lt;code&gt;COPY . /opt&lt;/code&gt; only copies in Python files, and excludes the &lt;code&gt;.venv&lt;/code&gt; folder. My choice of &lt;code&gt;/opt&lt;/code&gt; as the working directory is purely a matter of preference. With a Dockerfile like this, you can build and push the image into ECR and then deploy it as Lambda. But personally, I prefer to avoid going the Docker container route because it precludes you from being able to use Lambda's code editor in the AWS console, which I find is rather nice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging
&lt;/h2&gt;

&lt;p&gt;Coming into the Python world having done Java and C# development, I really didn't know the best practices for logging. And logging things properly is so important. I was used to creating logger objects in Spring Boot applications, using dependency injection to insert them into various classes, or using a factory class to instantiate them as class members. That second approach is actually closer to what I now consider best practices for logging in Python—only instead of passing around logger objects between classes and files, it's actually better to create a unique &lt;code&gt;logger&lt;/code&gt; object within each file at the top, and let functions within that file access it from the global namespace. In other words, &lt;em&gt;each&lt;/em&gt; file can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;getLogger&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_parameter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Notice how this function doesn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t accept the logger object as a parameter;
    it simply grabs it from the global namespace by convention.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Log message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_parameter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;my_parameter&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Omitted from that sample code is the logger configuration—let's talk about that. When I was starting out, I quickly realized the advantages of logging things in a JSON format rather than Python's default format. When deploying web services to the cloud, it can be immensely helpful to have all your logs consistently formatted using JSON, because then monitoring platforms like Datadog can parse your log events into a helpful, collapsible tree structure, allowing more complex searches on your log events.&lt;/p&gt;

&lt;p&gt;When I started out using Python, I assumed this meant I had to install some special package to handle the JSON logs for me, so I installed &lt;a href="https://www.structlog.org/en/stable/" rel="noopener noreferrer"&gt;structlog&lt;/a&gt;, which is a very popular logging package. Little did I know, I didn't actually need that at all! I spent a good amount of time implementing structlog in all of my team's projects, only to then spend more time ripping it out again—after realizing that Python's standard library was already more than capable of printing structured/JSON logs without the need for third-party packages.&lt;/p&gt;

&lt;p&gt;This has been a perennial lesson for me in Python: yes, there probably is a package on Github that does the thing you want, but you should always check to see if the thing you want can simply be done with just the standard library. In the case of JSON-formatted logs, I'll share some simplified code examples. First, let's create a file called &lt;code&gt;loggers.py&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traceback&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StreamHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;getLevelName&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="c1"&gt;# skip natural LogRecord attributes
# https://docs.python.org/3/library/logging.html#logrecord-attributes
&lt;/span&gt;&lt;span class="n"&gt;_RESERVED_ATTRS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asctime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exc_info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exc_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;funcName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;levelname&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;levelno&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lineno&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msecs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pathname&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;process&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relativeCreated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stack_info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskname&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;threadName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SimpleJsonFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LogRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;record_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_RESERVED_ATTRS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SeverityText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getLevelName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;levelno&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exception.stacktrace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;exc_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exc_val&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exception.message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc_val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exception.type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc_val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__class__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore
&lt;/span&gt;        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Attributes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_log_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disable_existing_loggers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SimpleJsonFormatter&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handlers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;console&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StreamHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ext://sys.stdout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handlers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;console&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see—within this file, we define a class called &lt;code&gt;SimpleJsonFormatter&lt;/code&gt;, which inherits from &lt;code&gt;logging.Formatter&lt;/code&gt;, as well as a function named &lt;code&gt;get_log_config&lt;/code&gt;. Then, in the &lt;code&gt;main.py&lt;/code&gt; file (or whatever your entrypoint Python file is), you can utilize this class and function like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;getLogger&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;logging.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dictConfig&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;loggers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_log_config&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;dictConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_log_config&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that you only want to call the &lt;code&gt;dictConfig&lt;/code&gt; function exactly once, ideally right at the beginning of launching your application. The &lt;code&gt;get_log_config&lt;/code&gt; function will ensure that every log message is passed through our &lt;code&gt;SimpleJsonFormatter&lt;/code&gt; class and all log messages will be printed as JSON objects. But best of all, you don't need to rely on third-party packages to achieve this! This can all be done with the standard library logging package.&lt;/p&gt;

&lt;p&gt;When I first tried out the structlog package, I thought a big advantage was that you could add in as many extra attributes as you wanted, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;structlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Log message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that functionality isn't unique to structlog at all. It can be supported just the same using the standard library, via the &lt;code&gt;extra&lt;/code&gt; parameter. So that same code snippet turns into something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Log message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this log statement gets passed to our &lt;code&gt;SimpleJsonFormatter&lt;/code&gt;, it comes in as an instance of the &lt;code&gt;logging.LogRecord&lt;/code&gt; class, and whatever values you include in the &lt;code&gt;extra&lt;/code&gt; parameter get embedded as a part of that LogRecord and then printed when we JSON encode it.&lt;/p&gt;

&lt;p&gt;I have given a very simplified example of a log formatter here, but there are many more powerful things you can do. For example, with additional log filters, you can utilize the &lt;a href="https://opentelemetry.io/docs/specs/otel/logs/data-model/#log-and-event-record-definition" rel="noopener noreferrer"&gt;OpenTelemetry standard&lt;/a&gt; to integrate traces and more.&lt;/p&gt;

&lt;p&gt;You can also rework my &lt;code&gt;get_log_config&lt;/code&gt; Python function into YAML syntax and use it directly with some ASGI servers. Here's the equivalent YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;disable_existing_loggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;formatters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;()&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SimpleJsonFormatter&lt;/span&gt;
&lt;span class="na"&gt;handlers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;console&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;logging.StreamHandler&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEBUG&lt;/span&gt;
    &lt;span class="na"&gt;formatter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
    &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ext://sys.stdout&lt;/span&gt;
&lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEBUG&lt;/span&gt;
  &lt;span class="na"&gt;handlers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;console&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if you build an application with something like FastAPI and uvicorn, you can utilize this custom logging when launching the application, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvicorn api:api &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--log-config&lt;/span&gt; logconfig.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When doing things this way, there's no need to explicitly call the &lt;code&gt;dictConfig&lt;/code&gt; function as uvicorn (or whatever other ASGI server you're using) will handle that for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding open-source software packages
&lt;/h2&gt;

&lt;p&gt;As I've mentioned, the Python community can often leave you with the impression that it's the wild, wild west out there. If you can think of some kind of application or library, chances are there are at least ten different versions of it already, all made by different people and all at various stages of development. The obvious example is the one I've already spoken about: package management. You've read why I think uv is the best tool for the job, but there's also pip, pipx, poetry, pdm, pipenv, pip-tools, conda, anaconda, and more.&lt;/p&gt;

&lt;p&gt;And if you take a look at web frameworks, there is FastAPI, Flask, Falcon, Django, and more. Or if you look at distributed computing and workflow orchestration, you might find packages like Airflow, Luigi, Dagster, or even pyspark. And the examples don't end there! If you look at blog software, the official Python site showcases &lt;a href="https://wiki.python.org/moin/PythonBlogSoftware" rel="noopener noreferrer"&gt;dozens upon dozens of different packages&lt;/a&gt; all relating to blogs, some of which haven't seen updates in over a decade.&lt;/p&gt;

&lt;p&gt;It can be incredibly confusing to a newcomer to Python to understand where to get started when it comes to identifying the right tooling to solve any given problem. I generally advocate for the same approach Google likes to see when they interview engineering candidates: namely, "has someone else already solved this problem?"&lt;/p&gt;

&lt;p&gt;Unfortunately, there is no hard and fast rule to determine what the most appropriate software package or tool is for the problem you want to solve. The answer is almost always "it depends." But I did discover a very powerful website that aids in the decision-making process: the &lt;a href="https://snyk.io/advisor/python" rel="noopener noreferrer"&gt;Snyk Open Source Advisor&lt;/a&gt;. The Snyk Open Source Advisor lets you search for any public Python package published to PyPI and provides a package health score, which is a composite of four different metrics. Pictured below is an example package health score for the &lt;a href="https://snyk.io/advisor/python/pelican" rel="noopener noreferrer"&gt;pelican library&lt;/a&gt;, which is the static site generator I use to create my blog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw33qyj5gounnum88f2xg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw33qyj5gounnum88f2xg.png" alt="Package Health for pelican" width="442" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The package health score ranks a number between 1 and 100, and the four metrics contributing to this score include Security, Popularity, Maintenance, and Community. These are immensely helpful—you certainly don't want to utilize a package that has known security holes. It's generally better to hone in on packages with a higher popularity score—though this becomes less true the more obscure your desired functionality is. The maintenance score is a quick way to determine if the project is still relevant and being updated, and the community score can help tell you how easy it would be to seek out support.&lt;/p&gt;

&lt;p&gt;I use the Snyk Open Source Advisor all the time as a Python developer. It doesn't completely solve the problem of determining which Python packages are the best tool for the job, but it sure helps make better informed decisions. There are some cases, of course, where you might deliberately choose a tool with a lower score than some other tool. Or you might find yourself comparing two frameworks that both serve the same function and have relatively high scores. And so experience is really the only critical factor there. But I have found 95% of the time I've been able to quickly determine the right package simply by looking at the Package Health score and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code formatting
&lt;/h2&gt;

&lt;p&gt;Earlier I mentioned that I've been installing a few tools globally. Let's focus specifically on three: ruff, lefthook, and mypy. ruff is a code formatting tool written by the same people who authored uv, and I absolutely love it. Previously, I had used a combination of black, pylint, isort, and others to handle code formatting, but I've found ruff is a sufficient replacement for them all.&lt;/p&gt;

&lt;p&gt;Wikipedia has a whole article on &lt;a href="https://en.wikipedia.org/wiki/Indentation_style" rel="noopener noreferrer"&gt;indentation style&lt;/a&gt;, which describes different code conventions for how you might format your code in different C-based languages. For instance, you might use the Allman style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you might use the K&amp;amp;R style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've witnessed one developer use one style, only to have another developer change things in a pull request with more substantive changes. This muddies the waters during code reviews, sometimes dramatically, when legitimate code changes are hiding between hundreds of lines of whitespace changes. A similar argument can be had with regard to tabs vs. spaces. Of course Python doesn't have this same problem with indentation because it enforces a single, consistent style as part of the syntax. However, there are other areas of code style where a consistent syntax is not enforced, and this is where tools like black or ruff come in.&lt;/p&gt;

&lt;p&gt;In Python, you might have debates over things like line length. For instance, the following two snippets of code are functionally the same, though cosmetically different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toml_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toml_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toml_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toml_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Personally, I find the latter version easier to read, and in this case it's the version you would get if you ran the code through a formatting tool like ruff. Though sometimes the tool will format the code in ways I wouldn't necessarily have chosen—but the point of code formatters is it takes away your choice, which is actually a very good thing. This way, we can avoid the scenario where one developer decides to change the style and you end up with long, muddied pull requests.&lt;/p&gt;

&lt;p&gt;Python encourages other rules, like import order, as part of &lt;a href="https://peps.python.org/pep-0008/#imports" rel="noopener noreferrer"&gt;PEP-8&lt;/a&gt;. Most people coming into Python for the first time don't realize there is a recommended best practice when it comes to sorting import statements. In fact, it's recommended that you split your imports into a maximum of three distinct groups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Standard library imports&lt;/li&gt;
&lt;li&gt; Related third-party imports&lt;/li&gt;
&lt;li&gt; Local application-specific imports&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each group is meant to be separated by a single empty line, and within each group, package imports (starting the line with &lt;code&gt;import&lt;/code&gt;) should come first while specific import (starting with &lt;code&gt;from&lt;/code&gt;) should come second. Furthermore, each of these groups and sub-groups should be sorted alphabetically, and any global imports (e.g. &lt;code&gt;from package import *&lt;/code&gt;) should be avoided altogether.&lt;/p&gt;

&lt;p&gt;Beyond what PEP-8 recommends, I advise never, ever using relative imports. The standard practice concedes there are some complex scenarios when relative imports are OK, but I say &lt;strong&gt;there is practically no scenario that justifies the use of relative imports in Python.&lt;/strong&gt; Unfortunately, ruff doesn't yet have preconfigured rules to ban the use of relative imports altogether, so it's incumbent on you to simply never use them.&lt;/p&gt;

&lt;p&gt;What's a relative import? Python allows you to write import statements like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;.package&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sibling&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;..pibling&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem with these is they are so tightly coupled to your filesystem that they almost scream, "I'm going to break the moment you try to share me!" If you prefix a package with a dot (&lt;code&gt;.&lt;/code&gt;) like the first statement above, it's telling Python to find either a file named package.py or a folder named package in the current directory. But this isn't necessary, because if you simply say &lt;code&gt;import package&lt;/code&gt; (without the dot) it will do the same thing! If you find yourself needing to use the dot, it's likely your package name conflicts with a reserved keyword or something in Python's standard library, in which case you should just rename your package or file. Similarly, there's no need to use &lt;code&gt;from . import sibling&lt;/code&gt;; &lt;code&gt;import sibling&lt;/code&gt; works just fine. Lastly, avoid using the double-dot to import something from outside your directory. If you're working with a separate module intended as a shared library, it's better to properly package it and install it into your environment rather than summoning the unholy mess of the double-dot import.&lt;/p&gt;

&lt;p&gt;So with that out of the way, let's see how I have ruff set up. As mentioned earlier, I first installed the ruff utility with &lt;code&gt;uv tool install ruff&lt;/code&gt;. This makes it globally available. Then, I created a configuration file at &lt;code&gt;~/.config/ruff/ruff.toml&lt;/code&gt; which looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[lint]&lt;/span&gt;
&lt;span class="py"&gt;extend-select&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"RUF100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# disallow unused # noqa comments&lt;/span&gt;
  &lt;span class="s"&gt;"I001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# isort&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This extends the default settings to include a couple of rules. &lt;a href="https://docs.astral.sh/ruff/rules/unused-noqa/" rel="noopener noreferrer"&gt;RUF100&lt;/a&gt; disallows unused &lt;code&gt;# noqa&lt;/code&gt; comments, which was a helpful recommendation from a friend, and &lt;a href="https://docs.astral.sh/ruff/rules/unsorted-imports/" rel="noopener noreferrer"&gt;I001&lt;/a&gt; ensures ruff automatically sorts imports per the PEP-8 standard.&lt;/p&gt;

&lt;p&gt;In addition to these rules in my global configuration, I've also been copying them to each pyproject.toml file any time I start a new project. This ensures that the rules always run, but beyond adding them to pyproject.toml files, it also ensures that anyone else looking at my code for a given package also follows the same rules. The syntax is only slightly different inside the pyproject.toml file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.ruff]&lt;/span&gt;
&lt;span class="py"&gt;lint.extend-select&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"RUF100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c"&gt;# dissallow unused # noqa comments&lt;/span&gt;
  &lt;span class="s"&gt;"I001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# isort&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you just need to run &lt;code&gt;ruff format .&lt;/code&gt; and &lt;code&gt;ruff check --fix .&lt;/code&gt; in sequence to apply these changes to your codebase. (The first is called formatting while the latter is called linting.) You can also make these commands run automatically either by hooking them into VS Code or using a pre-commit hook. I've opted to go with the latter route, and I've specifically been using a library called &lt;a href="https://github.com/evilmartians/lefthook" rel="noopener noreferrer"&gt;lefthook&lt;/a&gt; to accomplish this.&lt;/p&gt;

&lt;p&gt;Lefthook is basically a faster version of pre-commit, written in Go. It allows you to define a list of rules to run before checking in your code to Git. If any of the rules fail, the commit will also fail, so you're forced to deal with the failure before pushing your code. This setup involves creation of a lefthook.yml file in your repository. Here's an example lefthook.yml file that I've been using in one of my repositories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pre-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;uv-sort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/"&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pyproject.toml"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv-sort&lt;/span&gt;
    &lt;span class="na"&gt;python-lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/"&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.py"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruff check --fix {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;python-format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/"&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.py"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruff format {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;nbstripout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/"&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.ipynb"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nbstripout {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;js-lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontend/"&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.{js,mjs,cjs,ts,vue}"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx eslint --fix {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;js-format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontend/"&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.{js,mjs,cjs,ts,vue}"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx prettier --write {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, I have six pre-commit commands defined and only four of them are Python-related. I've segmented my code into different folders for the backend API (written in Python) and the frontend application (written in React.js). This is a good illustratration of how you can segment your lefthook rules to operate only on specific directories (and files) within your codebase.&lt;/p&gt;

&lt;p&gt;As you can see, the first command makes use of the &lt;code&gt;uv-sort&lt;/code&gt; utility previously installed via &lt;code&gt;uv tool install&lt;/code&gt;. This command ensures the dependencies in my pyproject.toml file are sorted. The next two commands are the two ways of invoking &lt;code&gt;ruff&lt;/code&gt; which I've already explained. And then I also run &lt;code&gt;npstripout&lt;/code&gt; to clean up any output from Jupyter notebook files before committing them. The last two commands run linting and formatting on a JavaScript codebase.&lt;/p&gt;

&lt;p&gt;Notably, I've opted to make use of the &lt;code&gt;stage_fixed&lt;/code&gt; directive too, which means that if any of these lefthook commands result in changes to the files I've staged, those changes will silently and automatically be included in my commit as well. This is a choice you may want to consider; I personally think it's useful because I don't ever think about formatting. Others may prefer to have their IDE automatically run formatting and linting commands upon save, and still others prefer not to do either, instead to forcing the user to create a second commit with any fixes.&lt;/p&gt;

&lt;p&gt;Noticeably absent from my &lt;code&gt;lefthook.yml&lt;/code&gt; file is any invocation of mypy, a static type checker. Static type checking is similar to linting, but it goes even further. Sometimes there are instances where you may not care when static type checking fails. Python is a dynamic language, meaning that variables are not strictly typed. Python also has a robust system of type hints allowing you to more explicitly designate how types flow between function and method calls. But at the end of the day, even type hints are still &lt;em&gt;hints&lt;/em&gt;, meaning nothing in the Python executable is going to actually enforce them. mypy helps verify that when type hints are specified, they are used consistently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mastodon.social/@hynek/113083295962409162" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fck9jnv6e2r8fzhv74tuq.png" alt="Post by Hynek" width="540" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hynek Schlawack, a well-known Pythonista and open source contributor, has &lt;a href="https://mastodon.social/@hynek/113083295962409162" rel="noopener noreferrer"&gt;recommended&lt;/a&gt; &lt;strong&gt;not&lt;/strong&gt; to run mypy as part of your pre-commit workflows. And Shantanu Jain, a CPython core developer and mypy maintainer, has &lt;a href="https://github.com/python/mypy/issues/13916" rel="noopener noreferrer"&gt;an excellent write-up&lt;/a&gt; of some of the gotchas that come with running mypy as a pre-commit hook. By default, mypy runs in an isolated environment, meaning it won't have access to your project's virtual environment and therefore won't be able to fully analyze type hints when your code makes use of dependencies. Additionally, pre-commit hooks only pass the changed/staged files by default, whereas mypy needs to see the entire repository to function correctly. I've also noticed nesting your code under other folders (like my &lt;code&gt;backend/&lt;/code&gt; directory) can also cause problems.&lt;/p&gt;

&lt;p&gt;Instead, I recommend setting up a Justfile at the root of your repository and defining a rule for manually running static type checks. Here's an example of a Justfile I've used in a past project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PYTHON_EXECUTABLE := `cd backend &amp;amp;&amp;amp; uv python find`
default:
    @just --list | grep -v "^    default$"

clean:
    @find . | grep -E "(/__pycache__$|\.pyc$|\.pyo$|\.mypy_cache$|\.ruff_cache$|\.pytest_cache$)" | xargs rm -rf

init-backend:
    @cd backend &amp;amp;&amp;amp; uv venv
    @cd backend &amp;amp;&amp;amp; uv sync

test-backend:
    @cd backend &amp;amp;&amp;amp; PYTHONPATH=$(pwd) uv run pytest .

typecheck:
    @MYPYPATH=backend mypy backend --python-executable {{PYTHON_EXECUTABLE}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key takeaway here is the final &lt;code&gt;typecheck&lt;/code&gt; rule which, as you can see, changes directories into the &lt;code&gt;backend/&lt;/code&gt; folder and calls &lt;code&gt;mypy&lt;/code&gt; with specific arguments. The &lt;code&gt;--python-executable&lt;/code&gt; argument is essential when you have dependencies, as it allows mypy to properly utilize the project's virtual environment.&lt;/p&gt;

&lt;p&gt;I've included three other rules here, which I'll briefly touch on, for those not familiar with Just.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The &lt;code&gt;default&lt;/code&gt; rule allows me to type &lt;code&gt;just&lt;/code&gt; from the terminal to print all available rules, excluding the default rule itself.&lt;/li&gt;
&lt;li&gt;  The &lt;code&gt;clean&lt;/code&gt; rule quickly deletes hidden files and folders that may appear after running the code, the tests, or formatting tools. (These files are also excluded in &lt;code&gt;.gitignore&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;  My &lt;code&gt;init-backend&lt;/code&gt; rule is meant to be run after cloning the repo for the first time.&lt;/li&gt;
&lt;li&gt;  My &lt;code&gt;test-backend&lt;/code&gt; rule runs the unit tests, which I'll touch on in the next section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My examples here showcase a project where I've thrown everything Python-related into a single folder. However, in many projects, you won't have to do this. I'm sharing this example because keeping everything at the root of the repository is considerably easier to deal with. In that case, you can simply remove all the &lt;code&gt;cd backend &amp;amp;&amp;amp;&lt;/code&gt; commands from the Justfile and the &lt;code&gt;root:&lt;/code&gt; directives from the lefthook.yml file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and debugging
&lt;/h2&gt;

&lt;p&gt;Being able to debug your code is critical. For those of you who come from a data science background, this may be a new concept. If you're using Jupyter notebooks, the whole concept of debugging is probably incorporated into your workflow already, at least conceptually. Juptyer notebooks build in the concept of breakpoints by forcing that you run your code line by line or block by block. But when we move beyond publishing Juptyer notebooks and into developing production-grade applications, developing good debugging practices is essential. Fortunately, the tooling in the ecosystem has improved dramatically in the last couple of years.&lt;/p&gt;

&lt;p&gt;Hand in hand with debugging comes the ability to run unit and integration tests. Python has a couple of different frameworks for testing including &lt;code&gt;pytest&lt;/code&gt; and &lt;code&gt;unittest&lt;/code&gt;, and there's some overlap between the two. I'll cover the basics of setting up unit tests and debugging in Python—without going into the moral question of using mocks, monkey patching, or other controversial techniques. If you're interested in those discussions, I'll include links to relevant articles at the end.&lt;/p&gt;

&lt;p&gt;When I first began coding in Python, VS Code integration was still fairly immature, and it made me want to reach for Jetbrains Pycharm instead. Pycharm has consistently maintained a very intuitive setup for debugging and testing Python, but it comes with a hefty price tag. However, in February 2024, I happened to catch an episode of the Test &amp;amp; Code podcast titled &lt;a href="https://testandcode.com/episodes/214-python-testing-in-vs-code" rel="noopener noreferrer"&gt;Python Testing in VS Code&lt;/a&gt; in wherein the host interviewed a Microsoft product manager and software engineer about overhaul improvements made to the Python extension for VS Code. And I agree—testing and debugging in VS Code is way better than it used to be. So much so that I no longer recommend purchasing Pycharm; VS Code (which is free) is more than sufficient.&lt;/p&gt;

&lt;p&gt;To get started with testing, I do recommend installing pytest as a dev dependency, using &lt;code&gt;uv add pytest --dev&lt;/code&gt;. I suggest creating a "test" folder in the root of your repository and prefix the files within with &lt;code&gt;test_&lt;/code&gt;. I also use the same prefix when naming test functions contained within those files. Here's an example: I might have files named test_api.py, test_settings.py, or test_utils.py, and within those files the test functions would look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mymodule.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;business_logic&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_functionality&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;business_logic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Business Logic should return True&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple test exercises one key aspect of unit testing: using &lt;em&gt;assertions&lt;/em&gt;. It runs the business logic function and asserts that the result returned is truthy. Moreover, the test will fail if the business logic function raises any kind of exception. If the test fails, we will see the string message in the test output. That alone is already a massively useful tool in the toolbox for remediating and even preventing bugs, but let's touch on a few other useful aspects about testing code in Python:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Capturing Exception&lt;/li&gt;
&lt;li&gt;  Fixtures&lt;/li&gt;
&lt;li&gt;  Mocks&lt;/li&gt;
&lt;li&gt;  Monkey Patching&lt;/li&gt;
&lt;li&gt;  Parameterization&lt;/li&gt;
&lt;li&gt;  Skip Conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond testing whether certain code returns specific values, it's also useful to craft tests that actually expect failure. This doesn't mean using exceptions as control flow (which is largely considered an anti-pattern), but rather testing that invalid inputs will reliably raise an exception. You can utilize the &lt;code&gt;raises&lt;/code&gt; function from pytest, which as you'll see here is actually a context manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;raises&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mymodule.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;business_logic&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_bad_input&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;business_logic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid input data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As shown above, you specify an exception type (which can be a tuple of multiple types when applicable) to the raises function. Then something inside the context manager block must raise that specific exception, otherwise the whole test will fail. Useful!&lt;/p&gt;

&lt;p&gt;However, sometimes the input for your business logic function is more complex than a simple scalar value. You might also want to re-use that same complex input across multiple test functions. While you could declare the input as a global variable, this can be problematic—particularly when you have functions that modify their own inputs. It's better to use fixtures. Here's an example of declaring a fixture that loads JSON from a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mymodule.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;business_logic&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;complex_input_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_data.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_functionality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complex_input_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;business_logic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complex_input_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Business Logic should return True&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, using the &lt;code&gt;@pytest.fixture&lt;/code&gt; decorator allows us to insert the name of our fixture function as an argument to any test function, injecting it automatically by pytest.&lt;/p&gt;

&lt;p&gt;A common challenge in Python applications and scripts is dealing with functions that perform business logic that can create side effects to, or dependencies on, external systems. For instance, maybe you have a function that calculates geographical distance between two points, but it needs to query a database first. Or maybe you have a function which reformats an API response and also saves it to S3.&lt;/p&gt;

&lt;p&gt;Now, I can already hear several of my colleagues screaming in my ear that you should simply write better code when functions are serving multiple purposes, that you use proper inversion of control, and so on. And while those are valid opinions, the purpose of this post isn't about debating best practices for architecture and testing. Rather, my goal is to empower newcomers to everything Python has to offer. And with that, I need to talk about monkey patching.&lt;/p&gt;

&lt;p&gt;Monkey patching is a feature in Python (as well as some other dynamically-typed languages) that allows you to modify the behavior of your application at runtime. Suppose you have a function like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mymodule.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_data&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;business_logic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# manipulate the data
&lt;/span&gt;    &lt;span class="n"&gt;result_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# save both forms to S3
&lt;/span&gt;    &lt;span class="n"&gt;cached_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transformed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result_data&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cached_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, when testing the business logic function, you want to be able to assert it performs the transformation correctly, without your unit tests actually creating new files in S3. You might be saying, "why test &lt;code&gt;business_logic&lt;/code&gt; at all? Just test &lt;code&gt;transform_data&lt;/code&gt;!" And in this overly simplified example, your instincts would be right. But let's suspend disbelief for a minute.&lt;/p&gt;

&lt;p&gt;If we want to test the business logic function and avoid writing anything to S3, we can use monkey patching to dynamically swap the call to &lt;code&gt;boto3.resource()&lt;/code&gt; with something else. Sometimes that can be a mock object, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MagicMock&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mymodule.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;business_logic&lt;/span&gt;

&lt;span class="nd"&gt;@patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mymodule.utils.boto3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_functionality_without_side_effects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mocked_boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;complex_input_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mocked_s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mocked_boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mocked_s3&lt;/span&gt;

    &lt;span class="n"&gt;mocked_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mocked_s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mocked_bucket&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;business_logic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complex_input_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUCKET_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FILENAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transformations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transformations key should be present&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;mocked_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_called_once&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's dive into what's happening here. I specified the patch decorator with the string &lt;code&gt;mymodule.utils.boto3&lt;/code&gt;. Notice I didn't directly import that path within the test script, but I did import the first part of it: &lt;code&gt;mymodule.utils&lt;/code&gt;. Pytest is then smart enough to figure out that from within that package, I further import boto3. The monkey patching happens right at that nested import statement, so instead of actually importing the real boto3 library, it returns a mock object any time I reference &lt;code&gt;boto3&lt;/code&gt; within that file. And that's useful!&lt;/p&gt;

&lt;p&gt;Now, based on my function definition from above, I do call &lt;code&gt;s3 = boto3.resource("s3")&lt;/code&gt;. And here's the beauty of mock objects in Python: any method or property of a mock object will also return another mock object. You can even override certain behaviors like &lt;code&gt;return_value&lt;/code&gt; and &lt;code&gt;side_effect&lt;/code&gt; of mock objects. Back to my test function, I declare the return value of the &lt;code&gt;resource()&lt;/code&gt; function on boto3 should be another mock object, which I've named &lt;code&gt;mocked_s3&lt;/code&gt;. Then I specify the return value of its &lt;code&gt;Bucket&lt;/code&gt; property should be another mock object named &lt;code&gt;mocked_bucket&lt;/code&gt;. I don't have to declare additional mock objects explicitly like this; I could have used something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mocked_boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_called_once&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I'm not a fan of extra-long lines like this, so when chaining calls I find it's cleaner and more readable to declare multiple mock objects as I've done above. So, if you're going to use mocks, I recommend this approach. Lastly, setting the &lt;code&gt;return_value&lt;/code&gt; property allows you to return a fixed value, whereas setting the &lt;code&gt;side_effect&lt;/code&gt; property to a function allows you to return data conditional on the input.&lt;/p&gt;

&lt;p&gt;Next, pytest also offers a parameterize decorator that lets you specify multiple inputs for a single test function. I grabbed this example straight from the &lt;a href="https://docs.pytest.org/en/stable/how-to/parametrize.html" rel="noopener noreferrer"&gt;pytest documentation&lt;/a&gt; to illustrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_input,expected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3+5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2+4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6*9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There may be cases where you want to skip running tests unless certain conditions are met. I've used the &lt;code&gt;skipif&lt;/code&gt; decorator to check whether a specific environment variable was present in the case of integration tests, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mymodule.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;business_logic&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.skipif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NEEDED_ENV_VAR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NEEDED_ENV_VAR was not set&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_integration&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;business_logic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, if you're using VS Code, the built-in testing integrations are really nice to work with. Get started by clicking the Testing icon on the left menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8kwrj1tdib9msbrqq6y8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8kwrj1tdib9msbrqq6y8.png" alt="Testing" width="148" height="59"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the blue button to Configure Tests, and then choose the pytest framework. Next, you'll need to choose the directory your project is in. In most cases this will be the root directory (&lt;code&gt;.&lt;/code&gt;), unless you're like me and you segment projects into a "backend" folder. If you &lt;em&gt;are&lt;/em&gt; like me, you may also need to specify your Python interpreter by clicking on the version of Python at the bottom right of the screen, clicking "Enter interpreter path...", and then typing out something like &lt;code&gt;backend/.venv/bin/python&lt;/code&gt;. If you're doing everything in the root of your repository, this will not be necessary, as VS Code likely auto-detected your virtual environment already.&lt;/p&gt;

&lt;p&gt;Voila! The tests should appear on the left pane along with Run and Debug buttons to execute them. Similarly, another helpful tool in being able to run your code directly inside VS Code while utilizing breakpoints. To get started, you'll want to click the Run and Debug icon on the left menu:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fswvkqwlhpqq0r4nbqqc5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fswvkqwlhpqq0r4nbqqc5.png" alt="Run and Debug" width="287" height="52"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first time you visit this tab, it will display a message that says, "To customize Run and Debug create a launch.json file." Simply click the link to create a launch.json file, which will bring up a command palette prompt. Choose "Python Debugger" as the first option, and then you're presented with a list of different templates. I usually pick "Python File" or "Python File with Arguments" to start, but you can add as many templates as you like. It will create a file that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"configurations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Python Debugger: Current File"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debugpy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"launch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"program"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${file}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"console"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integratedTerminal"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back in the Run and Debug tab, you should now see a dropdown menu on top (with your "Python Debugger" configuration selected by default), with a small, green play button to the right. Click this button to launch Python in debug mode. But be aware that if you're using the default "Current File" configuration, make sure you have your main.py file open and in-focus, and not your newly created launch.json file. Set breakpoints in your code by clicking the red dots just to the left of line numbers within your codebase, and now you're cooking with gas!&lt;/p&gt;

&lt;p&gt;My last bit on debugging is obviously uncontroversial, but my earlier section on utilizing mocks &lt;em&gt;can&lt;/em&gt; be divisive. A lot of seasoned developers actively discourage the use of mocks, or refine it to specific types of mocks (e.g. stubs and fakes). So if you're curious, here's some recommended reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://hynek.me/articles/what-to-mock-in-5-mins/" rel="noopener noreferrer"&gt;Don't Mock What You Don't Own&lt;/a&gt; by Hynek Schlawack&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://www.youtube.com/watch?v=CdKaZ7boiZ4" rel="noopener noreferrer"&gt;Mock Hell&lt;/a&gt; by Edwin Jung&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://martinfowler.com/articles/mocksArentStubs.html" rel="noopener noreferrer"&gt;Mocks Aren't Stubs&lt;/a&gt; by Martin Fowler&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://pythonspeed.com/articles/verified-fakes/" rel="noopener noreferrer"&gt;Fast tests for slow services: why you should use verified fakes&lt;/a&gt; by Itamar Turner-Trauring&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://www.youtube.com/watch?v=Xu5EhKVZdV8" rel="noopener noreferrer"&gt;Stop Mocking, Start Testing&lt;/a&gt; by Augie Fackler &amp;amp; Nathaniel Manista&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of the takeaways from those articles have to do with using too many mock objects, and then if you refactor your codebase later, it becomes super painful to find all of the mocks. They also recommend writing your tests to an interface (aka public API) rather than testing specific implementation details. Ask yourself the question, "How much could the implementation change, without having to change our tests?" And finally, several experts advise almost exclusively using fakes instead of mocks and recommend treating mocks as a tool of last resort.&lt;/p&gt;

&lt;p&gt;The larger morals of mocking and testing are out of scope for this article, but if you have questions about any of those points, I encourage you to peruse through those five links for more understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Python is a wildly powerful ecosystem that can seem overwhelming at first. Python has become the &lt;em&gt;lingua franca&lt;/em&gt; of data science and although it has plenty of detractors, it isn't going anywhere any time soon. So to briefly summarize all of my main points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Don't use relative imports.&lt;/li&gt;
&lt;li&gt;  Install uv and play around with its myriad features, both for managing dependencies and publishing packages.&lt;/li&gt;
&lt;li&gt;  Windows users: install WSL2 before installing anything else.&lt;/li&gt;
&lt;li&gt;  Configure loggers to output things in JSON format, fully utilizing the &lt;code&gt;extra&lt;/code&gt; argument.&lt;/li&gt;
&lt;li&gt;  Search the Snyk Open Source Advisor when looking for existing packages.&lt;/li&gt;
&lt;li&gt;  Use ruff for formatting and lefthook for automation, but make sure to handle mypy carefully.&lt;/li&gt;
&lt;li&gt;  Use the pytest integration in VS Code to better test and debug your code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hopefully this article illuminated some areas of development you were unsure about, equipped you with powerful new tools, and left you feeling empowered to get started. And let me know in the comments if you disagree with any of my recommendations or have better ideas!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was cross-posted from my own &lt;a href="https://gemovationlabs.com/what-i-wish-i-knew-about-python.html" rel="noopener noreferrer"&gt;tech blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Kubernetes Webhooks, Explained</title>
      <dc:creator>Gordon Myers</dc:creator>
      <pubDate>Sat, 09 Dec 2023 17:28:23 +0000</pubDate>
      <link>https://dev.to/soapergem/kubernetes-webhooks-explained-3lde</link>
      <guid>https://dev.to/soapergem/kubernetes-webhooks-explained-3lde</guid>
      <description>&lt;p&gt;Before the onset of the pandemic, I gave a talk at the Madison Cloud Native meetup titled "Aspect-Oriented Deployments with Kubernetes." This blog post is a long-overdue follow up to that talk, but if you're more of a visual person or if you've just stumbled across this post independently, I've included the video of my talk here. (This is extracted from a longer talk which can you still view &lt;a href="https://www.youtube.com/watch?v=V0ufVoSS414" rel="noopener noreferrer"&gt;here&lt;/a&gt;, wherein my portion starts at the 36:12 mark.)&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Cu7i57Wf8RQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;At the time, documentation on Kubernetes webhooks was scarce and almost non-existent. Now there is much better documentation available, and I would recommend perusing their article on &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/" rel="noopener noreferrer"&gt;Dynamic Admission Control&lt;/a&gt;. But I still have some unique insights to share, so I'll dive right into the nitty gritty details. I've broken this post up into a few sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Kubernetes?
&lt;/h2&gt;

&lt;p&gt;To understand how webhooks work, first it's necessary to explain a little bit of how Kubernetes works. Kubernetes is an open-source container orchestration platform and has become the gold standard in modern server deployment. When I began my software engineering career in 2008 as a PHP developer, I remember copying over PHP scripts onto bare metal web servers running Microsoft IIS. It was the best of times and the worst of times. It was certainly a very clunky way to handle deployments, as servers were treated more like pets than like cattle.&lt;/p&gt;

&lt;p&gt;Kubernetes starts from the premise that your application must be containerized, using the Open Container Interface (standard) -- the most well-known of which is Docker containers. Containerization fulfills the promise that Java set out to solve decades ago, ensuring that you can write your application once and then run it anywhere. Well, just as Java does still have the limitation that you can only run things where a JVM is installed, containers need a container runner installed, like Kubernetes.&lt;/p&gt;

&lt;p&gt;Kubernetes allows you to define a number of different types of objects including pods, deployments, services, ingress, and more. A pod is the smallest, most concrete type of object in the Kubernetes ecosystem. A pod is simply a collection of one or more containers that runs on one of the Kubernetes nodes. Often this is a single container running indefinitely, like a web server, but it can also be a one-off script that runs to completion, or a combination of containers that work in tandem with each other.&lt;/p&gt;

&lt;p&gt;There are a couple of higher-order objects that wrap the creation of pods with extra metadata, and fundamentally these are broken into two types: deployments and jobs. Deployments expect that the primary container will run forever, while jobs expect that the containers will terminate. Deployments automatically ensure that if a corresponding pod dies, Kubernetes will automatically spawn a replacement. And similarly with jobs, a pod that exits with a non-zero error code will be retried.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are webhooks?
&lt;/h2&gt;

&lt;p&gt;Kubernetes also defines two lesser-known object types called webhooks which affect the creation of pods: the &lt;code&gt;ValidatingWebhookConfiguration&lt;/code&gt; and the &lt;code&gt;MutatingWebhookConfiguration&lt;/code&gt;. These are also sometimes referred to as Validating Admission webhooks and Mutating Admission webhooks, respectively. When Kubernetes needs to create a pod, it first sends the definition (specification) of that pod to any registered Mutating webhooks. Those webhooks (because it could be plural!) either respond with an indication that the pod spec should be changed or not. Then, the potentially-updated pod definition is sent to any registered Validating webhooks, which in turn respond with a message to either allow the pod to be created, or not. Here's a simple diagram to illustrate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiswvp59hu4kbtsvp7xu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiswvp59hu4kbtsvp7xu.png" alt="Webhook diagram" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now allow me to explain the details of this API contract. These two types of webhook objects (like many Kubernetes object types) largely serve as pointers to somewhere else, where the real magic happens. And the main crux of a webhook configuration object, whether it's a validating or mutating type, is in a listener endpoint, inside of a custom web service. Thus it's important to understand that in order to leverage webhooks, you first need to define your own web service. This service can be deployed within the same Kubernetes cluster, though it doesn't strictly need to be (and in my talk I showcased an example of using an external AWS Lambda function as my webhook service).&lt;/p&gt;

&lt;p&gt;So all the magic happens within your custom web service, while the webhook configuration objects defined in Kubernetes simply tells Kubernetes where to go. From the Kubernetes perspective, there are really only two inputs here: the type of webhook (implicit in the object type you choose), and the pointer to your web server endpoint. Your web server then needs to be sure to respond in a way that conforms to the Kubernetes API contract for the chosen type of webhook. And I find that the best way to understand this is by showing off exactly what that looks like.&lt;/p&gt;

&lt;p&gt;In the case of a Validating Admission webhook, your custom listener would need to respond with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"uid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2441cee4-352b-11e9-85be-b8975a0ca30c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the case of a Mutating Admission webhook, your custom listener function might respond with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"uid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2441cee4-352b-11e9-85be-b8975a0ca30c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"patch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"W3sib3AiOiJhZGQiLCJwYXRoIjoiL3NwZWMvdm9sdW1lcy8tIiwidmFsdWUiOnsibmFtZSI6ImVudmNvbmZpZy12b2x1bWUifX1d"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So at a glance, the contract appears pretty simple. Both associate a unique ID with the response, and both output a Boolean flag called &lt;code&gt;allowed&lt;/code&gt;. In the case of Validating Admission webhooks, this is all they do. In the case of Mutating Admission webhooks, an additional field called &lt;code&gt;patch&lt;/code&gt; is included, which as you can see appears to contain some base64 encoded text. More on that in a minute.&lt;/p&gt;

&lt;p&gt;From a high level, you can think of webhooks as &lt;em&gt;functions that operate on your pods, before they launch&lt;/em&gt;. The webhook endpoint will receive a payload that defines a pod Kubernetes is trying to create, and then your endpoint will respond by saying first whether that operation should be allowed, and optionally if that pod definition should be modified before being launched. (Side note: because both types require &lt;code&gt;allowed: true&lt;/code&gt;, the mutating webhook can do everything that a validating webhook can do.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended Reading
&lt;/h3&gt;

&lt;p&gt;For the rest of this post, I'm going to assume that you already agree this is a good idea and dive further into the details. But if you want a more in-depth look at the concepts behind Kubernetes webhooks, let me recommend any of the following articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/" rel="noopener noreferrer"&gt;Dynamic Admission Control&lt;/a&gt; from the official Kubernetes documentation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/geekculture/back-to-basics-kubernetes-admission-webhooks-dbf6baffb0f1" rel="noopener noreferrer"&gt;Back to Basics: Kubernetes Admission Webhooks&lt;/a&gt; by Mina Omobonike&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/ibm-cloud/diving-into-kubernetes-mutatingadmissionwebhook-6ef3c5695f74" rel="noopener noreferrer"&gt;Diving into Kubernetes MutatingAdmissionWebhook&lt;/a&gt; by Morven Cao&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kelseyhightower/denyenv-validating-admission-webhook" rel="noopener noreferrer"&gt;Getting Started with Kubernetes Validating Admission Webhooks the FaaS Way&lt;/a&gt; by Kelsey Hightower&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is going on with that webhook API?
&lt;/h2&gt;

&lt;p&gt;If you're like me, you probably found it a bit confusing that the official standard has you return a JSON object which contains a base64 string. As I mentioned, when I first started looking at these 5 years ago, the documentation essentially did not exist. So I had to fire up some code I found from Banzai Cloud, detailed in &lt;a href="https://techblog.cisco.com/blog/inject-secrets-into-pods-vault-revisited" rel="noopener noreferrer"&gt;this blog post&lt;/a&gt;, and reverse engineer what they were doing by substituting my own web server and logging the responses. And what I discovered was even wilder than I expected: the base64 string contained within the JSON response is just &lt;em&gt;more JSON&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa5b1ujkinfqb9la990zy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa5b1ujkinfqb9la990zy.png" alt="I heard you like JSON" width="600" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the example I included in the last section, the base64 string actually decodes to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/spec/volumes/-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"envconfig-volume"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, that is simply a JSON list of objects. Why wasn't that list just directly embedded as the &lt;code&gt;patch&lt;/code&gt; element in the parent response? Which came first, the chicken or the egg? How many licks to get to the Tootsie Roll center of a Tootsie pop? There are some questions that just don't have any real answers. We just need to accept that this is the contract we have: the webhook returns JSON, with a base64 string representing a further nested JSON list. Taking a closer look at that list, it is clear that each object contains three keys: &lt;code&gt;op&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, and &lt;code&gt;value&lt;/code&gt;. As it turns out, this is part of the JSON Patch specification, which is codified in &lt;a href="https://datatracker.ietf.org/doc/html/rfc6902" rel="noopener noreferrer"&gt;RFC-6902&lt;/a&gt;. From the JSON Patch documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JSON Patch is a format for describing changes to a JSON document. It can be used to avoid sending a whole document when only a part has changed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For those who have worked with Kubernetes for any amount of time, you are surely familiar with the fact that K8s uses YAML as its format for defining objects. So why are we talking so much about JSON? Because, for all intents and purposes, JSON and YAML are basically &lt;em&gt;isomorphic&lt;/em&gt; -- meaning you can losslessly transform one into the other and back. And before you all come at me with pitchforks, I know that isn't technically true. Technically, YAML is a superset of JSON. But again, in the most common use cases, they are effectively isomorphic.&lt;/p&gt;

&lt;h2&gt;
  
  
  A primer on JSON patch
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://jsonpatch.com/" rel="noopener noreferrer"&gt;JSON Patch website&lt;/a&gt; itself already provides a very digestible explanation of JSON Patch, so if you're seriously considering writing your own Mutating Webhook Controllers, I strongly recommend you read through their documentation. But I will cover some highlights here as well. Let me begin by breaking down those three keys which are present on all JSON Patch objects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;op&lt;/code&gt; - Operation, can be one of: &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt;, &lt;code&gt;replace&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;path&lt;/code&gt; - The path to the specific JSON attribute that this operation will affect&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;value&lt;/code&gt; - The new value (omitted when the operation is &lt;code&gt;remove&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So that's pretty straightforward: in order to mutate your pod spec definition, you will either add, remove, or replace properties in the JSON document that gets translated from its original YAML definition. The path convention has its own specification but is pretty easy to understand. The path always begins with &lt;code&gt;/&lt;/code&gt; and each nested element down is delimited by additional &lt;code&gt;/&lt;/code&gt;s. When you're targeting an element to change or remove, you simply target it directly. And in the case of lists, you can separate the list indices with their index number and another slash. Finally, when adding new items to a list, you should end your path with &lt;code&gt;/-&lt;/code&gt; which is a special syntax to indicate you'll be appending an item.&lt;/p&gt;

&lt;p&gt;So in the example I gave above (inside the base64 string), I had used &lt;code&gt;/spec/volumes/-&lt;/code&gt; as the path. This assumes that there is already a list defined at &lt;code&gt;/spec/volumes&lt;/code&gt;. To make this a little more real, let me give an example of a pod spec in YAML, before and after mutation. Again, we will refer back to the example patch operation given earlier which was this: &lt;code&gt;{"op":"add","path":"/spec/volumes/-","value":{"name":"envconfig-volume"}}&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Before
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVerison&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mycoolwebapp&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mycoolwebapp&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;artifacts.server.com/docker/mycoolwebapp:latest&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-volume&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  After
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVerison&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mycoolwebapp&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mycoolwebapp&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;artifacts.server.com/docker/mycoolwebapp:latest&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-volume&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;envconfig-volume&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The importance of testing
&lt;/h3&gt;

&lt;p&gt;I hope you can already start to see how powerful Mutating Admission webhooks can be. They receive the pod spec definition (translated into JSON), parse it, and then make decisions on how to modify it. They relay those decisions by returning a list of JSON patch operations. But I also want to caution that this is also a very dangerous tool. In my talk at the meetup group, I quoted the famous Spiderman platitude that "with great power comes great responsibility." This is because your webhook function needs to be absolutely rock-solid, because if it fails or produces bad JSON, the result will be that your pods won't launch. So if you are someone who hardly ever writes unit tests, you should start writing them specifically for your webhook function. And even if you do write tests, when it comes to webhooks, &lt;em&gt;write more&lt;/em&gt;. I cannot stress enough how good your test coverage on your webhook function should be.&lt;/p&gt;

&lt;p&gt;Let's consider a quick example to illustrate why good testing is so important. In my previous example, I patched the &lt;code&gt;/spec/volumes&lt;/code&gt; list by adding an element to it. But what would have happened if the original pod specification hadn't contained a property at &lt;code&gt;/spec/volumes&lt;/code&gt; to begin with? (In other words, what if no volumes were defined?) Then trying to &lt;code&gt;add&lt;/code&gt; an element under &lt;code&gt;/spec/volumes/-&lt;/code&gt; would have resulted in a failure, and the intended pod would not have been launched. So the webhook code needs to be resilient enough to check for whether the property exists, and then conditionally return either &lt;code&gt;add&lt;/code&gt; to &lt;code&gt;/spec/volumes/-&lt;/code&gt; or perform a &lt;code&gt;replace&lt;/code&gt; on &lt;code&gt;/spec/volumes&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;Are you starting to understand why I'm saying that unit tests are so important?&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pod spec is transmitted
&lt;/h2&gt;

&lt;p&gt;So far I've focused entirely on the output of your function, and while I've made cursory allusions to the fact that the pod spec is transmitted, I haven't yet showcased that. So let's dive deeper. Technically what gets transmitted to your webhook listener endpoint (in the &lt;code&gt;POST&lt;/code&gt; body) is an &lt;code&gt;AdmissionReview&lt;/code&gt; object. At the end of the day, this is just another Kubernetes object type. But instead of transmitting it as YAML, it is sent to your endpoint as a JSON object. I have included below a highly truncated version of an AdmissionReview object that was actually sent to one of my webhook endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AdmissionReview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admission.k8s.io/v1beta1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"uid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2441cee4-352b-11e9-85be-b8975a0ca30c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"consul.myserver.com/kv"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jobs/my_job"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"vault.myserver.com/role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-job"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"vault.myserver.com/secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jobs/my_job"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"spec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"containers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-job"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"artifacts.myserver.com/docker/my-job:0.27.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"imagePullPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Always"&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have truncated this object significantly, and you can refer to &lt;a href="https://gist.github.com/soapergem/2e54fc150a07ac52765a210e778f9ac6" rel="noopener noreferrer"&gt;this gist&lt;/a&gt; if you want to see the full object. In the full object, the &lt;code&gt;metadata&lt;/code&gt; and &lt;code&gt;spec&lt;/code&gt; keys contain a lot more keys than what I have shown here. So let me focus on the important points.&lt;/p&gt;

&lt;p&gt;Earlier when I was explaining the response contract, I included a field named &lt;code&gt;uid&lt;/code&gt; under the &lt;code&gt;response&lt;/code&gt; key. That value needs to be copied from the original request, shown above under the &lt;code&gt;/request/uid&lt;/code&gt; path. And then secondly, you'll notice that there is a key under &lt;code&gt;/request/object&lt;/code&gt;. This is the JSON representation of your Kubernetes object that I was talking about earlier.&lt;/p&gt;

&lt;p&gt;You can see that this example features a Pod object, but in theory this could be a Service, a Deployment, or any other Kubernetes resource. The specific resource upon which your webhook receives callback events can be configured when creating the webhook object to begin with. But as this article is already long, I'll continue to focus just on the Pod use case and leave extrapolation to how this can be applied to other types as an exercise for the reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotations and AOP
&lt;/h2&gt;

&lt;p&gt;In my talk at the meetup group four years ago (which was titled "Aspect-Oriented Deployments with Kubernetes"), I tied a few concepts together. From &lt;a href="https://en.wikipedia.org/wiki/Aspect-oriented_programming" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The specific webhook example I showcased inspected the pod spec to determine whether or not certain annotations were present, and when so it would mutate the pod definition to facilitate an automatic connection to Hashicorp Vault in order to provide secure dependency injection. One reason for doing this at the time was that Role-Based Access Control (RBAC) had not been fully implemented in Kubernetes yet, so leaking secrets was a real concern. Nowadays RBAC is a mature and stable part of the Kubernetes ecosystem, so there is much less concern over using K8s secrets -- thereby making the whole purpose of my particular webhook application somewhat moot. But the underlying principles still teach elegant lessons in engineering principles like separation of concern.&lt;/p&gt;

&lt;p&gt;One of the most popular web frameworks in Java is Spring Boot, which has a framework for handling authentication called Spring Security. Spring Security provides an excellent object lesson in Aspect-Oriented Programming. For example, using this framework you could define a method called &lt;code&gt;getToken&lt;/code&gt; and apply one of Spring Security's built-in annotations such as &lt;code&gt;@Secured&lt;/code&gt; to ensure that the method can only be invoked when the current user has a specified role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Secured&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ROLE_VIEWER"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SecurityContext&lt;/span&gt; &lt;span class="n"&gt;securityContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SecurityContextHolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;securityContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthentication&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTokenForUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows developers to write methods that focus on their core functionality, without worrying about the details of cross-cutting concerns such as authentication and authorization, thereby providing effective modularity. Programming languages like Java provide a system where the annotations ultimately execute code. Even Python (where annotations are actually called decorators) does the same. But Kubernetes is not a programming language; it's an orchestration framework. And the objects in Kubernetes are all declarative, much like HTML, meaning Kubernetes annotations don't actually do anything. They simply are a piece of textual metadata on a pod (or another object).&lt;/p&gt;

&lt;p&gt;But webhooks are the missing link that allow you to take full advantage of annotations and AOP. You have full freedom to write your webhooks in whatever way you want, to respond to whatever conditions exist on a pod spec that you like. But I strongly advise that when writing custom Kubernetes webhooks, you focus entirely on which annotations are present (or not) as the mechanism for deciding whether to patch your objects. Strictly speaking, you don't have to do this. I could envision someone writing a webhook to deny admission on pods who have memory requests above a certain limit. (Though it is probably still better to simply rely on the internals of Kubernetes to handle that.) Or I could envision someone using a webhook to automatically rewrite container image paths from an old container registry to a new one. (Though it would be better to simply update your Helm charts with the new registry.) By responding to the presence of custom annotations, you will build resilient and maintainable applications that are useful and conform to a tried-and-true architecture pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Miscellaneous Pitfalls
&lt;/h2&gt;

&lt;p&gt;After my talk, a very nice man in the audience came up and asked if I had any specific challenges in building my webhook application that I may have left out of the talk. In the moment, I couldn't think of anything, but on later reflection it was immediately obvious that there is one major issue I had glossed over at the meetup group. That major issue again relates to the fact that Kubernetes objects, being entirely declarative, are &lt;em&gt;dumb&lt;/em&gt;. They don't go out of their way to tell you much, only what gets typed up and nothing more. But any time default values are inherited, they are not reflected in the specification sent to your webhook.&lt;/p&gt;

&lt;p&gt;In my particular application, this was a problem because my webhook application was responsible for dynamically rewriting the entrypoint of container images by prefixing an invocation of a custom binary application. So if the original entrypoint was &lt;code&gt;python main.py&lt;/code&gt;, the mutated form would be &lt;code&gt;envconfig -- python main.py&lt;/code&gt;. The problem is that not every pod spec will tell you what the original entrypoint is at all. In the vast majority of cases, the entrypoint is omitted in the deployment or job definitions because containers are meant to launch from their entrypoint by design. Typically, people will only specify the entrypoint in K8s pod specifications when they are overriding it with something custom. Thus, when the webhook is handed an AdmissionReview object containing the pod spec, no entrypoint will be present almost all of the time.&lt;/p&gt;

&lt;p&gt;The naive way to handle this problem would be to have your webhook server make a subprocess call to Docker to run &lt;code&gt;docker inspect&lt;/code&gt; on the provided image, and thereby look up the entrypoint (as well as other information) about the container image. But I hopefully don't need to elaborate on just how gross that would be. Fortunately, I came across an article titled &lt;a href="https://ops.tips/blog/inspecting-docker-image-without-pull/" rel="noopener noreferrer"&gt;Inspecting Docker images without pulling them&lt;/a&gt; which discusses the underlying container registry API behind Dockerhub, namely &lt;code&gt;registry.docker.io&lt;/code&gt;. This gave me the adequate hints needed to code up some simple web requests to be able to pull the metadata about containers without needing to install Docker and make procedure calls.&lt;/p&gt;

&lt;p&gt;However, while that code worked fine for Dockerhub, it needed to be adapted for Amazon ECR, and further adapted for Artifactory. With the proliferation of other container registry offerings such as Gitlab, Nexus, Google Artifact Registry, and so on, you might need to handle authentication differently for all of them. At the time, we were leveraging Artifactory and I confess there are caveats to their API that I still, to this day, do not understand. But I did want to provide a quick distillation of the article I mentioned into some simple Python code for others' benefit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_docker_registry_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://auth.docker.io/token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry.docker.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repository:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:pull&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;token_wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;token_wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_container_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;//&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry-1.docker.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;image_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;image_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;image_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
        &lt;span class="n"&gt;image_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry-1.docker.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;image_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;library/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;image_name&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/vnd.docker.distribution.manifest.v2+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry-1.docker.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_docker_registry_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your.container.registry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;NotImplementedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown container registry: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v2/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/manifests/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;image_tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;manifests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;digest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No config digest found for image: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v2/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/blobs/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That code snippet allows me to call &lt;code&gt;get_container_config()&lt;/code&gt; on any arbitrary Docker image. As you can see, I left off custom logic for other container registries, but it's generally not that difficult to provide them -- you usually just have to add an Authorization header.&lt;/p&gt;

&lt;p&gt;With that in mind, there are other default values that do not get propagated into the AdmissionReview object. Again, any time a property is omitted from the K8s object, the default value will be used, but your webhook will need some way of determining what that default value is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope this post has been instructive and useful. Are you already using principles from AOP in your Kubernetes deployments? Or have you been leveraging webhooks in another way? Also, do you know of better ways to cope with the pitfalls from needing to inspect container images from Docker registries? (Or have you had to deal with authentication issues from private ones?) Let me know in the comments if you have any lingering questions, or go ahead and share what creative uses for Kubernetes webhooks you've come up with!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was cross-posted from my own &lt;a href="https://gemovationlabs.com/kubernetes-webhooks-explained.html" rel="noopener noreferrer"&gt;tech blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>deployment</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
