<?xml version="1.0" encoding="UTF-8"?>
  <feed xmlns="http://www.w3.org/2005/Atom">
    <title>Python Wizardry by AppSignal</title>
    <subtitle>Dive deep into all things Python. If you are interested in learning more about how to build smooth, performant, and bug-free apps, you'll love this.</subtitle>
    <id>https://blog.appsignal.com</id>
    <link href="https://blog.appsignal.com"/>
    <link href="https://blog.appsignal.com/python-wizardry-feed.xml" rel="self"/>
    <updated>2026-04-19T11:00:00+00:00</updated>
    <author>
      <name>Roy Tomeij</name>
    </author>
    
  <entry>
    <title>Monitoring Django Query Performance with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2026/04/16/monitoring-django-query-performance-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2026/04/16/monitoring-django-query-performance-with-appsignal.html</id>
    <published>2026-04-16T00:00:00+00:00</published>
    <updated>2026-04-16T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Your app isn&#039;t slow, your queries are. Here&#039;s how to find and fix them using AppSignal.</summary>
    <content type="html">&lt;p&gt;Slow database queries are really a pain. It’s easy to blame Django for taking ages to process a request, but the real issue may lie in an SQL query not doing what it should be. These performance degrading queries are often hiding in plain sight. By the time you notice them, they are already affecting your end users.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.appsignal.com/&quot;&gt;AppSignal&lt;/a&gt; is an application performance monitoring (APM) tool with support for Python and Django out of the box. It can surface these exact issues with detailed insights for each individual request. In this short guide, you will learn how to quickly install it in your project. You will also find out about some common bad practices when querying in Django.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get this thing going!&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before you get started, you should have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;a href=&quot;https://www.djangoproject.com/&quot;&gt;Django&lt;/a&gt; project up and running&lt;/li&gt;
&lt;li&gt;A seeded database of your choice (MySQL, PostgreSQL, SQLite3)&lt;/li&gt;
&lt;li&gt;An application set up on &lt;a href=&quot;https://www.appsignal.com/&quot;&gt;AppSignal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up AppSignal in a Django Project&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s start off by setting up AppSignal in your existing Django project. Navigate to the root of your project&amp;#39;s directory and add the following packages inside the &lt;code&gt;requirements.txt&lt;/code&gt; file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;appsignal&lt;/code&gt;: the AppSignal library&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opentelemetry-instrumentation-django&lt;/code&gt;: automatically instruments Django&amp;#39;s request/response cycle, capturing each request as a trace&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opentelemetry-instrumentation-wsgi&lt;/code&gt;: instruments the WSGI layer so that spans are created at the server level before Django&amp;#39;s middleware even runs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opentelemetry-instrumentation-asgi&lt;/code&gt;: the async equivalent of the above, required if you&amp;#39;re running Django with an ASGI server like Uvicorn or Daphne&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;# requirements.txt
appsignal
opentelemetry-instrumentation-django
opentelemetry-instrumentation-wsgi
opentelemetry-instrumentation-asgi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Based on the SQL database your application uses, you need to install Django ORM-specific instrumentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/mysql.html&quot;&gt;MySQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/postgresql.html&quot;&gt;PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/sqlite.html&quot;&gt;SQLite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The AppSignal for Python integration will automatically use this instrumentation when the corresponding package is installed. To disable this instrumentation without uninstalling the package, use the &lt;a href=&quot;https://docs.appsignal.com/python/configuration/options.html#option-disable_default_instrumentations&quot;&gt;&lt;code&gt;disable_default_instrumentations&lt;/code&gt; configuration option&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, install the packages and run the AppSignal&amp;#39;s automated install tool, which will create &lt;code&gt;__appsignal__.py&lt;/code&gt; for you in the project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install -r requirements.txt
python -m appsignal install
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If the &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;python&lt;/code&gt; executables do not work, please use &lt;code&gt;pip3&lt;/code&gt; and &lt;code&gt;python3&lt;/code&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The tool will prompt you for an app name (previously created on AppSignal), as well as the &lt;strong&gt;Push API key&lt;/strong&gt;, which can be found in the &lt;a href=&quot;https://appsignal.com/redirect-to/app?to=info&quot;&gt;&lt;strong&gt;Push &amp;amp; deploy&lt;/strong&gt;&lt;/a&gt; section of your &lt;strong&gt;App settings&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Django requires a bit more configuration. Integrate the &lt;code&gt;manage.py&lt;/code&gt; file, and import the &lt;code&gt;appsignal&lt;/code&gt; module needs in the &lt;code&gt;manage.py&lt;/code&gt; file. Then, inside the &lt;code&gt;main&lt;/code&gt; method, call &lt;code&gt;appsignal.start&lt;/code&gt; to initialize instrumentation configured in the &lt;code&gt;__appsignal__.py&lt;/code&gt; (done automatically by the automated install tool).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# manage.py
#!/usr/bin/env python
&amp;quot;&amp;quot;&amp;quot;Django&amp;#39;s command-line utility for administrative tasks.&amp;quot;&amp;quot;&amp;quot;
import os
import sys

# Add this import statement
import appsignal

def main():
    &amp;quot;&amp;quot;&amp;quot;Run administrative tasks.&amp;quot;&amp;quot;&amp;quot;
    os.environ.setdefault(&amp;#39;DJANGO_SETTINGS_MODULE&amp;#39;, &amp;#39;&amp;lt;YOUR_APP_NAME_HERE&amp;gt;.settings&amp;#39;)

    # Add this method call
    appsignal.start()

    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            &amp;quot;Couldn&amp;#39;t import Django. Are you sure it&amp;#39;s installed and &amp;quot;
            &amp;quot;available on your PYTHONPATH environment variable? Did you &amp;quot;
            &amp;quot;forget to activate a virtual environment?&amp;quot;
        ) from exc
    execute_from_command_line(sys.argv)

if __name__ == &amp;#39;__main__&amp;#39;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to put your actual application name instead of the &lt;code&gt;&amp;lt;YOUR_APP_NAME_HERE&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; See &lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/django.html#Development%20setup&quot;&gt;more instructions for production setups&lt;/a&gt; using WSGI or ASGI.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;__appsignal__.py&lt;/code&gt; contains sensitive credentials (in this case the &lt;strong&gt;Push API key&lt;/strong&gt;). Please &lt;strong&gt;do not&lt;/strong&gt; commit this key to version control. Move this to your app&amp;#39;s security credentials or environment variables. &lt;a href=&quot;https://docs.appsignal.com/python/configuration/options.html#option-push_api_key&quot;&gt;See more details&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&amp;#39;s it for the AppSignal setup! Quick and easy, right? Your app is ready to be monitored.&lt;/p&gt;
&lt;p&gt;Next, let’s see how AppSignal tracks and traces database queries.&lt;/p&gt;
&lt;h2&gt;How AppSignal Tracks Database Queries&lt;/h2&gt;
&lt;p&gt;Once the server runs, AppSignal wraps Django&amp;#39;s database backend. Every SQL statement your ORM or raw &lt;code&gt;connection.cursor()&lt;/code&gt; generates becomes a &lt;em&gt;span&lt;/em&gt;: a timed unit of work attached to the request that triggered it.&lt;/p&gt;
&lt;p&gt;AppSignal breaks each request down into two components you&amp;#39;ll see throughout the dashboard:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;sql&lt;/strong&gt; — time spent waiting on the database&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;django&lt;/strong&gt; — time spent in Python (view logic, serialization, middleware)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a healthy request, those numbers are small and balanced. When something goes wrong, one of them dominates. Let&amp;#39;s see what kind of issues you can catch.&lt;/p&gt;
&lt;p&gt;&lt;Banner
  title=&quot;Monitor your Django query performance with AppSignal&quot;
  buttonText=&quot;Start monitoring for free&quot;
  buttonHref=&quot;https://appsignal.com/users/sign_up?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=monitoring_django_query_performance_appsignal_2026_04_12&amp;utm_content=banner_signup&quot;
  buttonGoal=&quot;monitoring_django_query_performance_appsignal_banner_signup&quot;
/&gt;&lt;/p&gt;
&lt;h2&gt;Identifying Slow Queries in the AppSignal Dashboard&lt;/h2&gt;
&lt;p&gt;First, you need to see the issues that have happened. Navigate to &lt;strong&gt;Performance &amp;gt; Issue list&lt;/strong&gt; in your AppSignal app.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/issue_list.png&quot; alt=&quot;Issue list shown in the AppSignal dashboard&quot; title=&quot;Issue list in the AppSignal dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Here, you have an overview of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The originating request (&lt;code&gt;Action name&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Its severity and status&lt;/li&gt;
&lt;li&gt;Mean time-to-completion, throughput, and impact on performance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By clicking on the originating request, you will open a full breakdown with all the details.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/request_summary.png&quot; alt=&quot;Request summary in the Issue list&quot; title=&quot;Request summary in the Issue list&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Samples&lt;/strong&gt; table shows the latest fetches, their durations, &lt;a href=&quot;https://www.appsignal.com/learning-center/what-is-an-n-plus-1-query&quot;&gt;N+1 query detection&lt;/a&gt;, and group breakdown.&lt;/p&gt;
&lt;p&gt;Press on a specific timestamp next.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/request_performance.png&quot; alt=&quot;Request sample breakdown&quot; title=&quot;Request sample breakdown&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This section shows the most intriguing details. The &lt;strong&gt;Sample breakdown&lt;/strong&gt; shows how much time has elapsed for each part of the request, for &lt;code&gt;sql&lt;/code&gt;, and &lt;code&gt;django&lt;/code&gt;, that is. This can help you pinpoint what you need to address. Is it the server or the query?&lt;/p&gt;
&lt;p&gt;AppSignal also associates locally available tags to the request. A waterfall timeline is also shown so you can see what happens in your request chronologically. Plus, this shows what query was performed at each step. Neat!&lt;/p&gt;
&lt;h2&gt;Common Django Query Problems and How to Spot Them&lt;/h2&gt;
&lt;p&gt;Now that AppSignal is surfacing your query data, you need to know what you&amp;#39;re looking for. Most Django performance problems fall into a handful of recurring patterns. Here are the most common ones and how the AppSignal dashboard helps you catch them.&lt;/p&gt;
&lt;h3&gt;The Good Old N+1 Queries&lt;/h3&gt;
&lt;p&gt;College peeps learn this early on. This is the most frequent database query problem that sneaks up on you more easily that you’d think. In fact, N+1 happens when you fetch a list of objects and execute an additional query for each one.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# views.py
def book_list(request):
    books = Book.objects.all()  # 1 query
    for book in books:
        print(book.author.name)  # 1 query per book → N+1
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The AppSignal &lt;strong&gt;Sample breakdown&lt;/strong&gt; shows N+1 pattern as a long stack of near-identical &lt;code&gt;sql&lt;/code&gt; calls in the waterfall timeline. The identical &lt;code&gt;SELECT&lt;/code&gt; statement is repeated tens or even hundreds of times, each with a tiny duration that really adds up to a significant total.&lt;/p&gt;
&lt;p&gt;AppSignal flags these directly in the Samples table with its built-in N+1 detection badge, so you &lt;strong&gt;do not&lt;/strong&gt; have to spot the pattern yourself.&lt;/p&gt;
&lt;p&gt;To fix the N+1 mishaps, you can almost always use &lt;code&gt;select_related&lt;/code&gt; (for foreign keys and one-to-one relations) or &lt;code&gt;prefetch_related&lt;/code&gt; (for many-to-many and reverse foreign keys):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;books = Book.objects.select_related(&amp;quot;author&amp;quot;).all()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Missing Database Indexes&lt;/h3&gt;
&lt;p&gt;Querying a small dataset usually does not cause issues. But, when data grows, a suboptimal query will definitely become a bottleneck. This happens when columns being filtered or ordered are not indexed. Django will complete the query without complaining, but database just has to scan the &lt;em&gt;entire&lt;/em&gt; table to give a response.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Filtering on a column with no index becomes a full table scan
orders = Order.objects.filter(status=&amp;quot;pending&amp;quot;).order_by(&amp;quot;created_at&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is to add an index on the relevant column. In Django, this is done directly in the model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Order(models.Model):
    status = models.CharField(max_length=20, db_index=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(fields=[&amp;quot;status&amp;quot;, &amp;quot;created_at&amp;quot;])
        ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you frequently filter on multiple columns together, a composite index (as shown above) is more efficient than two separate single-column indexes.&lt;/p&gt;
&lt;h3&gt;Unbounded QuerySets&lt;/h3&gt;
&lt;p&gt;Fetching more rows than you actually need is another way to degrade performance. A view that calls &lt;code&gt;Model.objects.all()&lt;/code&gt; without any limit will happily load every row in the table into memory. As the table grows, so does the response time and memory footprint.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Fine with 500 rows. A disaster with 500,000.
def export_view(request):
    records = AuditLog.objects.all()
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix depends on the use case. For paginated views, use Django&amp;#39;s built-in &lt;code&gt;Paginator&lt;/code&gt; or slice the QuerySet directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;recent_logs = AuditLog.objects.order_by(&amp;quot;-created_at&amp;quot;)[:100]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Repeated Identical Queries&lt;/h3&gt;
&lt;p&gt;Sometimes the same query is executed multiple times within a single request, not as part of an N+1 loop, but because different parts of the codebase independently ask for the same data. Django&amp;#39;s ORM does not cache QuerySet results across calls, and each evaluation hits the database.&lt;/p&gt;
&lt;h2&gt;Next Steps and Resources&lt;/h2&gt;
&lt;p&gt;Complex SQL queries can give you headaches, but queries that you cannot measure or understand will give you migraines. Now, if we stick with the same metaphor, using a tool like AppSignal to monitor direct SQL queries and Django calls can be like popping some ibuprofen.&lt;/p&gt;
&lt;p&gt;In this guide you’ve learned how to install AppSignal within your Django application, as well as what you can expect to get from AppSignal when performing the queries. From here, you may want to try setting up &lt;a href=&quot;https://docs.appsignal.com/application/notification-settings.html&quot;&gt;triggered alerts&lt;/a&gt; or &lt;a href=&quot;https://docs.appsignal.com/uptime-monitoring/setup.html&quot;&gt;uptime monitoring&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;AppSignal is &lt;a href=&quot;https://appsignal.com/users/sign_up&quot;&gt;free to start&lt;/a&gt; with and can be integrated within minutes in your project. Start spotting those pesky queries in no time!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Tracing a Slow Request Through Your Django App</title>
    <link rel="alternate" href="https://blog.appsignal.com/2026/04/14/tracing-a-slow-request-through-your-django-app.html"/>
    <id>https://blog.appsignal.com/2026/04/14/tracing-a-slow-request-through-your-django-app.html</id>
    <published>2026-04-14T00:00:00+00:00</published>
    <updated>2026-04-14T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Slow endpoints are difficult to detect because they don’t fail. Learn how to trace a slow request through your Django app using AppSignal.</summary>
    <content type="html">&lt;p&gt;Slow endpoints are difficult to detect because they don’t fail. They simply get slower and slower. Average latency may look fine, but that can be misleading.&lt;/p&gt;
&lt;p&gt;That’s why we need to look at other values, like &lt;code&gt;p90&lt;/code&gt; and &lt;code&gt;p95&lt;/code&gt;, which often reflect what’s really going on. For example, &lt;code&gt;p90&lt;/code&gt; represents the slowest 10% of requests, and &lt;code&gt;p95&lt;/code&gt; represents the slowest 5%. When these values increase, users start experiencing delays.&lt;/p&gt;
&lt;p&gt;For a backend developer, the problem is not necessarily how to make things faster; it’s rather how to identify where time is being spent. A slow request is not generally caused by one big query but by a series of small operations that add up.&lt;/p&gt;
&lt;p&gt;In this article, we are going to trace a slow request using a small demo application built with &lt;a href=&quot;https://github.com/django/django&quot;&gt;Django&lt;/a&gt;. We’ll use &lt;a href=&quot;https://appsignal.com/&quot;&gt;AppSignal&lt;/a&gt; to identify why it’s slow and how we can improve it.&lt;/p&gt;
&lt;h2&gt;Setting up the Demo Application&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll start with a simple project named &lt;code&gt;django_books_demo&lt;/code&gt;. It’s a data store for books, along with their authors and publishers.&lt;/p&gt;
&lt;p&gt;To create the project and an app, use the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;django-admin startproject django_books_demo
cd django_books_demo
python manage.py startapp inventory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s what the project structure should look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;django_books_demo/
│
├── manage.py
├── django_books_demo/
│   ├── settings.py
│   ├── urls.py
│   └── ...
│
└── inventory/
    ├── models.py
    ├── views.py
    └── migrations/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/project-structure.png&quot; alt=&quot;Project folder structure on VS code&quot; title=&quot;Project folder structure on VS code&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Installing AppSignal&lt;/h2&gt;
&lt;p&gt;Next, we install the &lt;a href=&quot;https://docs.appsignal.com/python.html&quot;&gt;AppSignal Python&lt;/a&gt; integration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install appsignal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following to your Django settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;APPSIGNAL_PUSH_API_KEY=&amp;quot;YOUR_APPSIGNAL_KEY&amp;quot;
APPSIGNAL_APP_NAME=&amp;quot;django-books-demo&amp;quot;
APPSIGNAL_ENVIRONMENT=&amp;quot;development&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we need to make sure the AppSignal agent is running when Django is running. We do this by adding the following to &lt;code&gt;manage.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import appsignal

appsignal.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you have done this, AppSignal will automatically monitor the performance of the Django requests you make.&lt;/p&gt;
&lt;h2&gt;Creating the Data Models&lt;/h2&gt;
&lt;p&gt;Our demo application is simple. It only manages books and their relationships.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=200)

class Publisher(models.Model):
    name = models.CharField(max_length=200)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to run the following commands to actually create the database tables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py makemigrations
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Populating the Database&lt;/h2&gt;
&lt;p&gt;We need to populate the database to simulate real-world usage.&lt;/p&gt;
&lt;p&gt;First, we start the shell:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, we fill the database with a large amount of data to demonstrate ORM’s shortcomings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random

from inventory.models import Author, Book, Publisher

authors = [Author.objects.create(name=f&amp;quot;Author{i}&amp;quot;) for i in range(20)]
publishers = [Publisher.objects.create(name=f&amp;quot;Publisher{i}&amp;quot;) for i in range(10)]

for i in range(1000):
    Book.objects.create(
        title=f&amp;quot;Book{i}&amp;quot;,
        author=random.choice(authors),
        publisher=random.choice(publishers),
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have &lt;strong&gt;1,000 books&lt;/strong&gt; in our database, it’s easier to spot where ORM is slowing things down.&lt;/p&gt;
&lt;h2&gt;Creating a Slow Django Endpoint&lt;/h2&gt;
&lt;p&gt;Let’s set up an endpoint that returns all the books in our database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.http import HttpResponse, JsonResponse

from .models import Book

def home(request):
    return HttpResponse(&amp;quot;Hello books app&amp;quot;)

def books(request):
    books = Book.objects.all()

    data = []
    for book in books:
        data.append(
            {
                &amp;quot;title&amp;quot;: book.title,
                &amp;quot;author&amp;quot;: book.author.name,
                &amp;quot;publisher&amp;quot;: book.publisher.name,
            }
        )

    return JsonResponse(data, safe=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then add this endpoint in &lt;code&gt;urls.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.urls import path

from inventory.views import books, home, test_error

urlpatterns = [
    path(&amp;quot;&amp;quot;, home, name=&amp;quot;home&amp;quot;),
    path(&amp;quot;books/&amp;quot;, books),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance, our code looks perfectly normal and doesn&amp;#39;t contain any obvious issues. However, it has a performance pitfall that’s common in Django applications.&lt;/p&gt;
&lt;p&gt;Inside the loop, it fetches the related objects:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;book.author.name
book.publisher.name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, because the relationships were not prefetched, this will cause extra database queries for each access.&lt;/p&gt;
&lt;p&gt;This is known as the &lt;a href=&quot;https://blog.appsignal.com/2025/07/09/how-to-avoid-nplus1-queries-in-django-python.html&quot;&gt;&lt;em&gt;N+1 query&lt;/em&gt; problem&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Triggering the Slow Request&lt;/h2&gt;
&lt;p&gt;First, start a development server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then send a few requests to the endpoint:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://127.0.0.1:8000/books/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Each request will generate trace data that AppSignal captures.&lt;/p&gt;
&lt;h3&gt;Step 1: Finding the Slow Endpoint&lt;/h3&gt;
&lt;p&gt;Send a few hits to the endpoint. Then, check the AppSignal dashboard, and go to: &lt;strong&gt;Performance -&amp;gt; Issue list&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Issue list&lt;/strong&gt; includes all endpoints used by the application, along with request counts and response times.&lt;/p&gt;
&lt;p&gt;These are the results for our demo application:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Mean response time&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;GET /books/&lt;/td&gt;
&lt;td&gt;4.81 sec&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET /home&lt;/td&gt;
&lt;td&gt;1.07 sec&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;/books/&lt;/code&gt; endpoint clearly stands out.&lt;/p&gt;
&lt;p&gt;The response time is &lt;strong&gt;over 4 seconds&lt;/strong&gt;, which is considered long for a simple API request.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/actions-dashboard.png&quot; alt=&quot;AppSignal actions dashboard highlighting slow endpoints&quot; title=&quot;The AppSignal Actions dashboard highlights slow endpoints. The /books/ route shows significantly higher response times compared with the rest of the application&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Step 2: Analyzing Request Performance&lt;/h3&gt;
&lt;p&gt;AppSignal also provides &lt;a href=&quot;https://www.appsignal.com/tour/performance&quot;&gt;performance charts&lt;/a&gt; to visualize how request latency varies over a period of time.&lt;/p&gt;
&lt;p&gt;The charts offer two types of data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mean response time:&lt;/strong&gt; Average request latency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90th percentile latency (&lt;code&gt;p90&lt;/code&gt;):&lt;/strong&gt; Slower request latency that impacts user experience&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/performance-chart.png&quot; alt=&quot;AppSignal’s performance chart displaying request latency over a period of time&quot; title=&quot;AppSignal’s performance chart displaying request latency over a period of time. The two lines on the graph represent average response time or mean, and 90th percentile. The spikes on the graph represent slower request execution times for the /books/ endpoint during testing.&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The spikes on the graph indicate slower request execution times for the &lt;code&gt;/books/&lt;/code&gt; endpoint during testing: &lt;strong&gt;almost 6 seconds&lt;/strong&gt;. This is a confirmation that the &lt;code&gt;/books/&lt;/code&gt; endpoint is performing poorly.&lt;/p&gt;
&lt;p&gt;Interestingly, the &lt;strong&gt;Slow queries&lt;/strong&gt; section on AppSignal is empty. This is because the slower request execution times aren’t caused by a single expensive query. Instead, the request is executing hundreds of small queries, which is in accordance with the &lt;strong&gt;N+1 query anti-pattern&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;Banner
  title=&quot;Want to see how AppSignal can help you optimize your Django app?&quot;
  buttonText=&quot;Try AppSignal for free&quot;
  buttonHref=&quot;https://appsignal.com/users/sign_up?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=tracing_a_slow_request_through_your_django_app_2026_04_11&amp;utm_content=banner_signup&quot;
  buttonGoal=&quot;tracing_a_slow_request_through_your_django_app_banner_signup&quot;
/&gt;&lt;/p&gt;
&lt;h2&gt;Understanding the Root Cause&lt;/h2&gt;
&lt;p&gt;Django QuerySets are &lt;strong&gt;lazy&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When we access a related object, such as &lt;code&gt;book.author&lt;/code&gt;, Django makes an additional database query unless the relationship has already been loaded.&lt;/p&gt;
&lt;p&gt;So, here’s what’s happening in our loop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 query to fetch books
+1000 queries to fetch authors
+1000 queries to fetch publishers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The number of queries is directly proportional to the number of results. This isn’t a problem with the database itself, it’s actually an ORM issue, and request tracing helps us understand the difference between the two.&lt;/p&gt;
&lt;h2&gt;Fixing the Slow Query&lt;/h2&gt;
&lt;p&gt;Django offers an optimization feature called &lt;a href=&quot;https://docs.djangoproject.com/en/6.0/ref/models/querysets/#select-related&quot;&gt;&lt;code&gt;select_related()&lt;/code&gt;&lt;/a&gt; that lets you retrieve related data in the query. This way, it can load all the models upfront, to avoid running thousands of extra queries.&lt;/p&gt;
&lt;p&gt;We will update the view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def books(request):

    books = Book.objects.select_related(&amp;quot;author&amp;quot;, &amp;quot;publisher&amp;quot;)

    data = []
    for book in books:
        data.append(
            {
                &amp;quot;title&amp;quot;: book.title,
                &amp;quot;author&amp;quot;: book.author.name,
                &amp;quot;publisher&amp;quot;: book.publisher.name,
            }
        )

    return JsonResponse(data, safe=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The updated query tells Django to &lt;strong&gt;join the tables into one query&lt;/strong&gt; instead of running multiple.&lt;/p&gt;
&lt;h2&gt;Comparing the Results&lt;/h2&gt;
&lt;p&gt;As you can see in the table below, after refreshing the endpoint once the fix has been applied, AppSignal shows that the request’s response time is much shorter:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Query strategy&lt;/th&gt;
&lt;th&gt;Response time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Original implementation&lt;/td&gt;
&lt;td&gt;N+1 queries&lt;/td&gt;
&lt;td&gt;3–6 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimized implementation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;select_related()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~60 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Here’s one of the traces recorded after the fix has been applied:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GET /books/ → 60 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the response time has been dramatically reduced:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/performance-samples.png&quot; alt=&quot;AppSignal trace after optimizing the query&quot; title=&quot;After optimizing the query with select_related, the response time drops from nearly nine seconds to well under a second&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This only confirms that removing the N+1 queries has a significant impact on request times.&lt;/p&gt;
&lt;h2&gt;A Repeatable Debugging Workflow&lt;/h2&gt;
&lt;p&gt;Performance debugging is much easier when a standard process is followed.&lt;/p&gt;
&lt;p&gt;Here’s how it works with AppSignal:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Identify which endpoints are slow using the &lt;strong&gt;Actions&lt;/strong&gt; dashboard.&lt;/li&gt;
&lt;li&gt;Open a representative &lt;strong&gt;trace&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Determine what kind of operation is taking up most of the time.&lt;/li&gt;
&lt;li&gt;Optimize the code.&lt;/li&gt;
&lt;li&gt;Measure the improvement using new traces.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This kind of process represents a much more systematic approach to performance debugging.&lt;/p&gt;
&lt;h2&gt;Why Observability Matters&lt;/h2&gt;
&lt;p&gt;Performance issues can creep up on you as the volume of your data increases. Without proper monitoring, issues like N+1 queries can go unnoticed until user experience begins to degrade and noticeable delays occur.&lt;/p&gt;
&lt;p&gt;Observability tools like AppSignal offer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Request-level visibility&lt;/strong&gt; lets you see what actually happens during a single request, so you’re not guessing where the time is going.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database vs. app time&lt;/strong&gt; helps you quickly figure out if the slowdown is coming from queries or your own code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance trends&lt;/strong&gt; show how things change over time, so you can catch problems early, before they reach users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measurable before and after performance comparisons&lt;/strong&gt; prove that your changes have actually improved performance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This way, your team can identify bottlenecks and ensure that optimizations are indeed making the system faster.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Slow endpoints don’t usually manifest themselves with obvious error messages. They sneak up on you quietly, stealing time and resources as your app grows.&lt;/p&gt;
&lt;p&gt;In our example, &lt;a href=&quot;https://appsignal.com/&quot;&gt;AppSignal&lt;/a&gt; has helped us debug a slow Django endpoint and identify a classic issue called N+1 queries. With a simple change to our ORM, we cut our request time from several seconds to a few milliseconds.&lt;/p&gt;
&lt;p&gt;For a backend developer, request tracing offers a powerful perspective on how your app is behaving. With good observability tools in place, it’s much easier to debug and resolve performance issues.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Tracking Celery Task Failures in Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2026/04/09/tracking-celery-task-failures-in-python.html"/>
    <id>https://blog.appsignal.com/2026/04/09/tracking-celery-task-failures-in-python.html</id>
    <published>2026-04-09T00:00:00+00:00</published>
    <updated>2026-04-09T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s explore how Celery task failures behave and how you can keep track of everything that goes wrong.</summary>
    <content type="html">&lt;p&gt;Whenever you place an order on Amazon (or any other e-commerce site for that matter), you get that “order placed successfully” notification almost instantly. But did you know that there’s much more to the whole experience than meets the eye?&lt;/p&gt;
&lt;p&gt;In Python applications, &lt;a href=&quot;https://docs.celeryq.dev/en/stable/&quot;&gt;Celery&lt;/a&gt; is the major driver behind the whole thing. The tasks that take time are queued and sent to brokers. Workers pick them up and process them, and the application still runs normally, independent of what is going on in the background.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll explain how Celery task failures behave and how you can keep track of everything that goes wrong. We will reproduce a task failure through an example and observe the entries in worker logs. Then, we will use &lt;a href=&quot;https://appsignal.com/&quot;&gt;AppSignal&lt;/a&gt; to track the failure with full context.&lt;/p&gt;
&lt;h2&gt;Celery as a Default for Background Jobs in Python&lt;/h2&gt;
&lt;p&gt;Celery is often in charge of some time-consuming tasks like processing huge datasets or calling external APIs. It plays a huge role in why websites feel so fast. You know how you get an instant success message whenever you place an order or submit a form? Well, there’s a lot of heavy lifting going on behind the scenes.&lt;/p&gt;
&lt;p&gt;However, the main problem with Celery is that background tasks fail quietly. From the outside, everything will look healthy because it will manage to succeed after a couple of retries. However, if you take a closer look, you’ll see that multiple errors occurred before this success. In fact, teams generally discover these failures only when a user asks why a certain action didn’t go through.&lt;/p&gt;
&lt;p&gt;Celery uses default worker logs to report errors, which is fine when your system is small, but once you’ve got tons of tasks running across multiple workers and queues, it all gets messy fast. Suddenly, engineering teams are left squinting at lines of text, trying to figure out what happened.&lt;/p&gt;
&lt;h2&gt;The Problem with Celery’s Defaults&lt;/h2&gt;
&lt;p&gt;Celery’s defaults make background jobs easy to run, but they can also hide reliability issues. A task may fail several times and move through retries before it eventually succeeds, so from the outside everything looks fine while the real errors stay hidden in worker logs. To understand how these defaults can impact your system&amp;#39;s stability, let’s look at a practical example of how Celery handles a failing task.&lt;/p&gt;
&lt;h3&gt;Celery Runs, Fails, and Retries&lt;/h3&gt;
&lt;p&gt;Celery has built-in retry mechanisms that automatically rerun tasks after an exception. In our application example, we reproduced a task that fails intentionally most of the time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#A task that fails often
@app.task(bind=True, max_retries=3)
def fragile_task(self):
    attempt = self.request.retries + 1
    retries_left = self.max_retries - self.request.retries

    print(
        f&amp;quot;fragile_task attempt {attempt} &amp;quot;
        f&amp;quot;(task_id={self.request.id}, retries_left={retries_left})&amp;quot;
    )

    time.sleep(1)

    if random.random() &amp;lt; 0.7:
        raise self.retry(exc=Exception(&amp;quot;Simulated failure&amp;quot;), countdown=2)

    return &amp;quot;Success&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our task is designed to fail 70% of the time, with a maximum of three retries before it finally gives up. So when we trigger the task using our &lt;code&gt;producer.py&lt;/code&gt; script, Celery will send the job to the message broker (&lt;a href=&quot;https://www.rabbitmq.com/&quot;&gt;RabbitMQ&lt;/a&gt; in our case) and the worker will begin to execute it.&lt;/p&gt;
&lt;p&gt;This is our &lt;code&gt;producer.py&lt;/code&gt; code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from tasks import add, fragile_task

# Send a simple task
result1 = add.delay(4, 6)
print(&amp;quot;Add task sent:&amp;quot;, result1.id)

# Send several fragile tasks
for i in range(5):
    result = fragile_task.delay()
    print(&amp;quot;Flaky task sent:&amp;quot;, result.id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, the worker begins processing tasks in the background.&lt;/p&gt;
&lt;h3&gt;Exceptions Go to Logs Nobody Watches&lt;/h3&gt;
&lt;p&gt;Celery comes in handy when offloading work to background workers. The flow is simple: you push a task to a queue, the worker picks it up, and your web app stays fast. However, the downside is that Celery failures can be easy to miss.&lt;/p&gt;
&lt;p&gt;When a task fails, the traceback just gets printed in worker process logs. So, unless you are actively tailing those logs, there is a good chance they’ll stay under the radar. There’s no central place where you can see failures or track trends over time.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s do a test run and look at the logs the worker produces:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/celery-worker-logs.png&quot; alt=&quot;Celery worker logs showing task retries and simulated failure exceptions&quot; title=&quot;Celery worker logs showing task retries and simulated failure exceptions&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Here, we see the worker connect to the RabbitMQ broker (&lt;code&gt;amqp://guest@127.0.0.1:5672&lt;/code&gt;) and register two tasks: &lt;code&gt;tasks.add&lt;/code&gt; and &lt;code&gt;tasks.fragile_task&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The first one, &lt;code&gt;add&lt;/code&gt;, executes immediately and succeeds.&lt;/p&gt;
&lt;p&gt;Next, the worker receives several &lt;code&gt;fragile_tasks&lt;/code&gt; jobs, which are tasks that we intentionally made unstable with a 70% chance of failing. So when a failure occurs, Celery logs the exception, such as &lt;code&gt;fragile_task attempt 1&lt;/code&gt;, and schedules a retry (&lt;code&gt;Retry in 2s: Exception(&amp;#39;Simulated failure&amp;#39;)&lt;/code&gt;). After retrying, most tasks eventually succeed, which is why you later see &lt;code&gt;succeeded in ~1s: &amp;#39;Success&amp;#39;&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;If you look closely at the test run above, you’ll notice the worker logs include entries like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[2026-03-04 19:27:10,301: INFO/MainProcess] Task tasks.fragile_task[5cc33856-7038-4d54-9952-f7236c0accf9] received
[2026-03-04 19:27:10,303: INFO/MainProcess] Task tasks.fragile_task[7173fb28-6ea9-4517-a5fc-d8035600fde8] received
[2026-03-04 19:27:10,303: INFO/ForkPoolWorker-2] Task tasks.fragile_task[167b7c9e-7d46-4792-b7ab-d4dd0fbd4e6b] retry: Retry in 2s: Exception(&amp;#39;Simulated failure&amp;#39;)
[2026-03-04 19:27:10,303: INFO/ForkPoolWorker-3] Task tasks.fragile_task[7173fb28-6ea9-4517-a5fc-d8035600fde8] retry: Retry in 2s: Exception(&amp;#39;Simulated failure&amp;#39;)
[2026-03-04 19:27:10,303: INFO/ForkPoolWorker-10] Task tasks.fragile_task[5cc33856-7038-4d54-9952-f7236c0accf9] retry: Retry in 2s: Exception(&amp;#39;Simulated failure&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From these logs, we can see that the task failed multiple times before reaching its retry limit. Although this information technically exists, it is buried deep in the worker logs. As the system scales and multiple workers run at the same time, the logs quickly become noisy and difficult to interpret.&lt;/p&gt;
&lt;h3&gt;Retries Hide Real Failures&lt;/h3&gt;
&lt;p&gt;A task might fail a few times, retry automatically, and eventually succeed. For an untrained eye, this looks like a win. But there are exceptions behind the scenes that you didn’t see. For example, a task that has failed four times and succeeded in its fifth attempt has already raised four exceptions, consumed worker time, and caused a delay for other jobs.&lt;/p&gt;
&lt;p&gt;You are unable to see the failures unless you take the time to dig through the worker logs. But this is not practical, especially when you’re running a distributed system with a massive user base that generates loads of log data.&lt;/p&gt;
&lt;h3&gt;You Find Something Is Broken From Users&lt;/h3&gt;
&lt;p&gt;Sometimes, a background job fails silently for some users, and retries fail to fix it, as well. However, you only realize this days later, after a user complains about a payment that didn’t go through or a confirmation email that never arrived.&lt;/p&gt;
&lt;p&gt;And that’s the issue with Celery and the model it follows. It simply raises an exception and forgets about it, which is good for throughput, but it can cause failures to slip by under the radar.&lt;/p&gt;
&lt;h2&gt;What AppSignal Captures&lt;/h2&gt;
&lt;p&gt;In the previous section, we intentionally created failing tasks and triggered them. While the worker logs showed retries and exceptions, we had to manually track task IDs across multiple log lines to understand what actually happened.&lt;/p&gt;
&lt;p&gt;This is where AppSignal is useful. It automatically instruments Celery tasks and records every execution for each run. Instead of digging through messy logs, it &lt;a href=&quot;https://www.appsignal.com/tour/errors&quot;&gt;groups and displays task failures&lt;/a&gt; in a structured way, making it easy to see what went wrong and why.&lt;/p&gt;
&lt;h3&gt;Automatic Task Instrumentation&lt;/h3&gt;
&lt;p&gt;We’ve already seen how demanding it is to go through worker logs when undertaking investigations. In a distributed system, it’s almost impossible since you are supposed to manually scan thousands of lines of worker logs. AppSignal addresses this issue by removing the need to sift through those.&lt;/p&gt;
&lt;p&gt;Once it’s installed, all background tasks are immediately instrumented. It’s capable of gathering structured data regarding each task&amp;#39;s execution, including retry attempts, exceptions, and performance metrics. Now, you can see clearly how background jobs are behaving.&lt;/p&gt;
&lt;h3&gt;Exceptions With Full Tracebacks&lt;/h3&gt;
&lt;p&gt;By now, AppSignal has been installed, and you’ve set it up to monitor your Python application. You can immediately see how quickly it provides a full trace of task retries.&lt;/p&gt;
&lt;p&gt;AppSignal records the complete traceback alongside rich contextual information. You no longer have to dig through logs manually. It shows you exactly what went wrong; you’ve got the exception type, stack trace, when the error occurred, and which worker executed the task. Errors are also grouped by exception type, which makes recurring problems easier to spot.&lt;/p&gt;
&lt;h3&gt;Retry Visibility&lt;/h3&gt;
&lt;p&gt;Logs can get pretty messy, even in a simple app, but imagine what it would look like in a distributed system that has thousands of tasks, each generating tons of worker logs and going through multiple retries.&lt;/p&gt;
&lt;p&gt;AppSignal makes keeping track of all that a breeze. It shows important details like how many retries happened, when they occurred, and exactly where the error popped up in your code. Without it, a task that fails a few times before finally succeeding might look totally fine on the surface, even though it’s actually failing repeatedly behind the scenes.&lt;/p&gt;
&lt;h3&gt;Task Context and Arguments&lt;/h3&gt;
&lt;p&gt;As a software engineer, you already understand the significance of context. When debugging a failure, most of us want to know why something happened and whether it was caused by a new change or something else entirely.&lt;/p&gt;
&lt;p&gt;AppSignal helps you out by recording task arguments that led to a failure. This allows you to correlate errors with specific inputs while automatically filtering sensitive information. When you can see both the exception and task context together, debugging becomes much faster.&lt;/p&gt;
&lt;h3&gt;Installing AppSignal&lt;/h3&gt;
&lt;p&gt;Enabling &lt;a href=&quot;https://www.appsignal.com/python/celery-monitoring&quot;&gt;Celery monitoring&lt;/a&gt; with AppSignal is pretty simple: you only need to install it once in the application environment and initialize it when the worker starts.&lt;/p&gt;
&lt;p&gt;The full setup process is covered in the &lt;a href=&quot;https://docs.appsignal.com/python/installation&quot;&gt;AppSignal Python installation guide&lt;/a&gt;, which explains how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install the AppSignal package&lt;/li&gt;
&lt;li&gt;Configure your &lt;strong&gt;API key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Start the application with monitoring enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Seeing Failures as They Happen&lt;/h2&gt;
&lt;p&gt;In distributed systems, task failures often get buried in worker logs. A task may eventually succeed after a few retries, making the issue seem temporary. But when a task fails repeatedly across multiple workers, the real problem can easily go unnoticed.&lt;/p&gt;
&lt;p&gt;AppSignal surfaces these failures immediately, as it captures exceptions directly from the worker process. Instead of hunting through logs across multiple machines, it allows you to view failures as structured events.&lt;/p&gt;
&lt;p&gt;In this example, we’ve instrumented the Celery worker with AppSignal and triggered several failing &lt;code&gt;fragile_task&lt;/code&gt; executions to demonstrate how it all works.&lt;/p&gt;
&lt;p&gt;&lt;Banner
  title=&quot;Stop letting Celery tasks fail in silence&quot;
  buttonText=&quot;Start monitoring with AppSignal&quot;
  buttonHref=&quot;https://appsignal.com/users/sign_up?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=tracking_celery_task_failures_python_2026_04_09&amp;utm_content=banner_signup&quot;
  buttonGoal=&quot;tracking_celery_task_failures_python_banner_signup&quot;
/&gt;&lt;/p&gt;
&lt;h3&gt;Celery Task Failures Appear in the Errors Dashboard&lt;/h3&gt;
&lt;p&gt;When the failing tasks run, AppSignal catches the exceptions and bundles identical failures together. Instead of spamming you with a log entry for every retry, it aggregates them into a single issue on the dashboard.&lt;/p&gt;
&lt;p&gt;This makes it easy to spot a recurring problem, as you can see in this AppSignal dashboard:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/appsignal-error-list.jpg&quot; alt=&quot;AppSignal dashboard displaying grouped task errors&quot; title=&quot;AppSignal dashboard displaying grouped task errors&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This dashboard interface shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Failing background task, &lt;code&gt;run/tasks.fragile_task&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Exception grouped under one issue&lt;/li&gt;
&lt;li&gt;Six total occurrences&lt;/li&gt;
&lt;li&gt;The time the error last occurred&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This grouping is a game-changer in distributed workloads. A single bug can produce dozens (or even hundreds of retries) across workers. AppSignal spots these patterns right away, so you don’t have to dig through logs to see what’s going on.&lt;/p&gt;
&lt;h3&gt;Inspecting the Error Details&lt;/h3&gt;
&lt;p&gt;Click the grouped error to get a detailed view of the failure. You’ll find critical debugging information (normally scattered across worker logs) grouped together.&lt;/p&gt;
&lt;p&gt;Here’s what the dashboard shows:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/appsignal-error-traceback.png&quot; alt=&quot;AppSignal error detail page&quot; title=&quot;AppSignal error detail page&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You will find:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Error message&lt;/strong&gt;: This is the exception raised during task execution; in this example, the task fails with &lt;code&gt;Simulated failure&lt;/code&gt; message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full traceback&lt;/strong&gt;: The stack trace shows exactly where the error occurred in the code. Here, the failure originates from &lt;code&gt;tasks.py&lt;/code&gt; inside the &lt;code&gt;fragile_task&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task metadata&lt;/strong&gt;: Each task execution includes a message ID generated by the queue system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution environment&lt;/strong&gt;: The worker hostname (&lt;code&gt;Nehemiah&lt;/code&gt;) identifies which worker processed the task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue information&lt;/strong&gt;: The task was executed from the &lt;code&gt;celery&lt;/code&gt; queue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timestamp&lt;/strong&gt;: The exact time the failure occurred is recorded for every sample.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All this helps you see whether failures were isolated events or if they actually happened repeatedly across multiple workers. This context is very important in distributed task systems. Plus, as mentioned, you don’t have to dig for this information yourself. You get a clear view of which task failed, where it ran, and how often this failure occurred.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/celery-task-attributes.png&quot; alt=&quot;AppSignal displaying task attributes&quot; title=&quot;AppSignal displaying task attributes&quot;/&gt;&lt;/p&gt;
&lt;p&gt;These attributes provide deeper operational context such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retry count (&lt;code&gt;celery.retries&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;worker hostname (&lt;code&gt;celery.hostname&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;queue routing information&lt;/li&gt;
&lt;li&gt;task state (&lt;code&gt;FAILURE&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Debugging no longer requires you to work on something vague that failed in the queue. It has become a specific Celery task that failed with this exception across multiple executions.&lt;/p&gt;
&lt;h3&gt;Why This Matters in Production Systems&lt;/h3&gt;
&lt;p&gt;Failures are pretty easy to spot in small development environments, but what if the production system runs dozens of Celery workers with multiple processes each?&lt;/p&gt;
&lt;p&gt;For example, the worker in this demo runs with 12 concurrency, as seen here:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2026-04/concurrency-in-worker-demo.jpg&quot; alt=&quot;Celery worker terminal output&quot; title=&quot;Celery worker terminal output&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This means up to twelve tasks execute simultaneously. When failures occur under these conditions, logs can grow quickly and conceal the underlying issue.&lt;/p&gt;
&lt;p&gt;AppSignal solves this problem by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Capturing exceptions automatically&lt;/li&gt;
&lt;li&gt;Grouping failures by type&lt;/li&gt;
&lt;li&gt;Attaching execution metadata&lt;/li&gt;
&lt;li&gt;Showing trends in the error dashboard&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of reading logs line by line, you can immediately identify which task is failing and why.&lt;/p&gt;
&lt;h2&gt;Patterns to Watch For&lt;/h2&gt;
&lt;p&gt;Observability is more than just capturing individual failures. As an engineer, you will often encounter patterns that reveal deeper issues in background job systems. Observability tools monitoring dashboards, just like we have seen with AppSignal, make these patterns visible before they become huge production problems.&lt;/p&gt;
&lt;h3&gt;Retry Storms&lt;/h3&gt;
&lt;p&gt;A retry helps a task recover from temporary failures, but the downside is they can hide deeper problems. We can equate retries to safety nets of distributed systems designed to handle temporary failures. It is great for improving a system&amp;#39;s resilience but can also mask underlying problems.&lt;/p&gt;
&lt;p&gt;Often a task retrying several times before succeeding generates multiple failures that might go unnoticed. If you are not monitoring the retry count alongside the success rates, you might miss the fact that tasks are succeeding only after multiple attempts.&lt;/p&gt;
&lt;p&gt;The same exception might appear multiple times and then succeed. The retry is not fixing the problem but rather delaying failure. If you see the same exception recurring across multiple retries, then there is a high likelihood you are dealing with a deeper issue that will disrupt the system rather than a random error.&lt;/p&gt;
&lt;h3&gt;One Task Dragging Down the Queue&lt;/h3&gt;
&lt;p&gt;Background tasks have distinct behaviors. Some tasks are processed almost immediately, while others take a few seconds. When a slow task occupies a worker process for too long, it reduces the number of workers available for other tasks. This results in slow queue latency, delayed task execution, and reduced system throughput.&lt;/p&gt;
&lt;h3&gt;External Dependency Failures&lt;/h3&gt;
&lt;p&gt;There are background tasks that have to interact with some external systems, like APIs, databases, or messaging services. These tasks can appear broken when those dependencies fail, even though they are not the real problem.&lt;/p&gt;
&lt;p&gt;Tracebacks can reveal these issues. Some common examples include &lt;code&gt;ConnectionError, TimeoutError&lt;/code&gt;, &lt;code&gt;HTTPError&lt;/code&gt;. In these cases, the task logic may be correct, but the external service it depends on could be unavailable or the response may be slow.&lt;/p&gt;
&lt;h2&gt;Alerting on What Matters&lt;/h2&gt;
&lt;p&gt;Dashboards help when investigating issues, but alerts ensure problems are detected immediately. In background processing systems like Celery, tasks often run silently without direct user interaction. This means failures can continue for hours before anyone notices unless alerting is configured properly.&lt;/p&gt;
&lt;p&gt;Effective alerting involves targeting signals that indicate deeper system issues rather than temporary noise. It’s important to always target high-value signals, as they help find the root cause of a problem.&lt;/p&gt;
&lt;p&gt;High-value alerts typically include:&lt;/p&gt;
&lt;h3&gt;Error Rate Spikes&lt;/h3&gt;
&lt;p&gt;If a task that normally succeeds suddenly begins to fail frequently, it signals a bug, a configuration issue, or a missing dependency. Alerting on unusual error rates helps teams react quickly before failures affect larger workflows.&lt;/p&gt;
&lt;p&gt;You can easily spot the tasks that are slowing down the system and fix the issue by going through the data.&lt;/p&gt;
&lt;h3&gt;New Exception Types&lt;/h3&gt;
&lt;p&gt;When you spot new exceptions that you haven’t noticed before, new system changes might be at fault. If engineers manage to detect them early enough, they will be able to investigate and fix everything before the issue spreads.&lt;/p&gt;
&lt;h3&gt;Task Duration Anomalies&lt;/h3&gt;
&lt;p&gt;A task that normally completes in milliseconds but suddenly starts taking seconds can slow down queue processing. Alerting on &lt;a href=&quot;https://www.appsignal.com/tour/performance&quot;&gt;performance anomalies&lt;/a&gt; helps identify slow queries and overloaded services.&lt;/p&gt;
&lt;p&gt;Engineers’ goal is to know when something important breaks. For example, if a task such as &lt;code&gt;send_welcome_email&lt;/code&gt; starts failing repeatedly, the team should receive an alert immediately rather than by discovering the issue through user complaints later.&lt;/p&gt;
&lt;p&gt;Well-designed alerts focus on high-value signals that help indicate deeper system issues.&lt;/p&gt;
&lt;h2&gt;Example: Debugging a Silent Failure&lt;/h2&gt;
&lt;p&gt;Now, let’s consider a background task called &lt;code&gt;process_payment&lt;/code&gt;. Its job is to charge a user’s card and update their account once the payment succeeds. Most payments complete successfully, but suddenly there is a small percentage that begins to fail. Since the task is configured to retry, some payments eventually succeed after multiple attempts. From the outside, the system appears healthy.&lt;/p&gt;
&lt;p&gt;Without proper visibility, the engineering team will only become aware of this problem when a user submits a support ticket. They will then search through worker logs trying to reconstruct what happened, often without enough context to reproduce the failure.&lt;/p&gt;
&lt;p&gt;AppSignal makes investigation easier. The error dashboard quickly reveals a spike in &lt;code&gt;stripe.error.CardError&lt;/code&gt; exceptions coming from the &lt;code&gt;process_payment&lt;/code&gt; task. Instead of scattered log entries, failures are grouped together under a clean interface.&lt;/p&gt;
&lt;p&gt;Looking at the error samples shows additional context:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User IDs associated with the failures&lt;/li&gt;
&lt;li&gt;Timestamps of each occurrence&lt;/li&gt;
&lt;li&gt;Traceback returned by the Stripe API&lt;/li&gt;
&lt;li&gt;Number of retries that occur&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From there, the pattern becomes clear. Failures occur when a specific card type fails Stripe’s validation rules. Once the pattern is visible, fixing the issue becomes easier.&lt;/p&gt;
&lt;h2&gt;Wrap-Up&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/celery/celery&quot;&gt;Celery&lt;/a&gt; enables workers to run in the background, which represents a big advantage for modern software systems. A task is sent to a broker (RabbitMQ or Redis) and then assigned to workers for processing. Tasks throwing exceptions are retried without affecting the functioning of the whole application. However, these retries can also make it difficult to notice underlying issues that eventually trigger system failures.&lt;/p&gt;
&lt;p&gt;In most cases, background tasks fail quietly, especially with no proper visibility. In huge distributed systems, exceptions disappear into worker logs. Over time, it becomes a challenge for teams to discover problems before users report them.&lt;/p&gt;
&lt;p&gt;Observability platforms help in addressing this challenge. Instrumenting Celery tasks enables engineering teams to see failures as they happen, track retries, and inspect the full context behind each exception.&lt;/p&gt;
&lt;p&gt;If you’re already running Celery in production, the next step is straightforward. Follow the &lt;a href=&quot;https://docs.appsignal.com/python/installation&quot;&gt;Python installation guide&lt;/a&gt; in the AppSignal documentation to add monitoring to your application and start capturing real task behavior.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Improve Query Performance Using Python Django QuerySets</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/12/03/improve-query-performance-using-django-python-querysets.html"/>
    <id>https://blog.appsignal.com/2025/12/03/improve-query-performance-using-django-python-querysets.html</id>
    <published>2025-12-03T00:00:00+00:00</published>
    <updated>2025-12-03T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s improve the performance of your queries using Django QuerySets.</summary>
    <content type="html">&lt;p&gt;When developing web applications with Django, your interaction with the database impacts overall application performance. Django&amp;#39;s Object-Relational Mapper (ORM) is a powerful ally that offers an intuitive way to work with your data through abstractions called QuerySets. These are your primary tools for fetching, filtering, creating, and managing data.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll explore fundamental — yet highly effective — techniques to optimize your Django QuerySets. Our goal is to learn how to write database queries that are both functional and efficient. This ensures your applications remain fast, responsive, and scalable.&lt;/p&gt;
&lt;p&gt;First, let&amp;#39;s dig deeper into why database performance shouldn&amp;#39;t be viewed as just a technical concern, but rather a cornerstone of a successful web application.&lt;/p&gt;
&lt;h2&gt;The Importance of Database Performance in Web Applications&lt;/h2&gt;
&lt;p&gt;A database is often the workhorse of a web application. It serves and stores the data that brings your application to life. Its performance is not an isolated metric. It has far-reaching consequences for both the end-user experience and the operational health of your servers.&lt;/p&gt;
&lt;p&gt;So, let’s briefly discuss how performance impacts user experience and server resources in an application.&lt;/p&gt;
&lt;h3&gt;Impact on User Experience&lt;/h3&gt;
&lt;p&gt;Slow database queries are a primary culprit behind high page load times. Users’ patience is a finite resource, and when an application feels sluggish, they are more likely to leave. Over time, poor performance can lead to lower engagement and create a negative perception of your brand, eroding user trust and portraying your application as unreliable. Optimizing database performance, therefore, is directly tied to user satisfaction and retention.&lt;/p&gt;
&lt;h3&gt;Impact on Server Resources&lt;/h3&gt;
&lt;p&gt;Inefficient queries also place a strain on your server infrastructure. By forcing the database to perform complex joins or scan large tables, they consume excessive hardware resources. This pressure also limits your application&amp;#39;s ability to scale. In extreme cases, an overloaded database can become unresponsive, leading to cascading failures that risk an application outage. Writing efficient QuerySets ensures your application makes judicious use of server resources. This leads to a stable, scalable, and cost-effective system.&lt;/p&gt;
&lt;p&gt;Read our guide &lt;a href=&quot;https://blog.appsignal.com/2024/07/31/monitor-the-performance-of-your-python-django-app-with-appsignal.html&quot;&gt;Monitor the Performance of Your Python Django App with AppSignal&lt;/a&gt; for more on this topic.&lt;/p&gt;
&lt;h2&gt;Understanding Django QuerySets&lt;/h2&gt;
&lt;p&gt;Before going through specific optimization techniques, it is important to have an overview of what QuerySets are, their core characteristics, and why they are a cornerstone of Django&amp;#39;s database interaction model.&lt;/p&gt;
&lt;h3&gt;What is a QuerySet?&lt;/h3&gt;
&lt;p&gt;A &lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/&quot;&gt;Django QuerySet&lt;/a&gt; is a list of objects from your database. It is an abstraction layer that represents a collection of database queries. It allows you to retrieve, filter, order, and annotate data from your database using Python, rather than writing raw SQL. When you interact with your Django models, you are working with QuerySets.&lt;/p&gt;
&lt;p&gt;Think of a QuerySet as a &amp;quot;recipe&amp;quot; for fetching data. You can define this recipe step-by-step, and Django will only execute when it absolutely has to.&lt;/p&gt;
&lt;h3&gt;Key Characteristic of QuerySets: Laziness&lt;/h3&gt;
&lt;p&gt;One of the most important characteristics of QuerySets is their &amp;quot;laziness.&amp;quot; This means that the act of creating or modifying a QuerySet does not immediately result in any database activity. For example, consider the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from myapp.models import User
# No database query has been executed yet
active_users = User.objects.filter(is_active=True)
recent_signups = active_users.filter(date_joined__year=2025)
ordered_users = recent_signups.order_by(&amp;#39;-date_joined&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, even though you have defined &lt;code&gt;active_users&lt;/code&gt;, &lt;code&gt;recent_signups&lt;/code&gt;, and &lt;code&gt;ordered_users&lt;/code&gt;, Django has not sent a SQL query to the database. Instead, Django simply builds an internal representation of the query.&lt;/p&gt;
&lt;p&gt;The database query is executed when the QuerySet is evaluated. Evaluation typically occurs when you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Iterate over the QuerySet&lt;/strong&gt;: This is perhaps the most common scenario for evaluation. When you start a loop (like a &lt;code&gt;for&lt;/code&gt; loop) over a QuerySet, Python needs the actual items to iterate through. At this point, Django executes the database query to fetch the rows that match your QuerySet&amp;#39;s criteria. The database returns the data, and Django begins to instantiate model objects one by one (or in internal chunks for efficiency) as your loop progresses. The key here is that the loop&amp;#39;s body needs the data to operate on each item.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slice it with a &lt;code&gt;step&lt;/code&gt; parameter&lt;/strong&gt;: Basic slicing (like &lt;code&gt;ordered_users[:5]&lt;/code&gt; to get the first five users) often returns another unevaluated QuerySet. Instead, if you use a &lt;code&gt;step&lt;/code&gt; in the slice (e.g., &lt;code&gt;[::2]&lt;/code&gt; to get every second user), you force immediate evaluation. To determine which items to return when a step is involved, Django typically needs to retrieve the entire result set from the database first. Once all potential items are fetched into memory, Python can apply the stepping logic to select the appropriate items. This is different from simple limit/offset slicing, which the database can often handle directly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pickle or cache it&lt;/strong&gt;: &lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/#pickling-querysets&quot;&gt;Pickling&lt;/a&gt; converts a Python object into a byte stream, often for storage or transmission. If you attempt to pickle a QuerySet, you generally want to pickle its results, not just the abstract query definition. So, to serialize the data, you (or the system) must first evaluate the QuerySet to fetch that data from the database. Similarly, when you cache a QuerySet, you usually aim to store its results to avoid future database hits. This process needs an initial database query to retrieve the results that you (or the system) will then place into the cache.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Call &lt;code&gt;repr()&lt;/code&gt; or &lt;code&gt;len()&lt;/code&gt; on it&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Calling &lt;code&gt;repr()&lt;/code&gt; on a QuerySet will trigger its evaluation. To provide a string representation, Django usually needs to fetch at least a subset of the results to display (e.g., &lt;code&gt;&amp;lt;QuerySet [&amp;lt;User: Alice&amp;gt;, &amp;lt;User: Bob&amp;gt;, ...]&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Calling &lt;code&gt;len()&lt;/code&gt; on a QuerySet also causes evaluation. To determine the number of items in the QuerySet, Django will execute the query, retrieve all the matching objects into memory, and then count how many there are. This is why it&amp;#39;s generally less efficient than using the &lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/#django.db.models.query.QuerySet.count&quot;&gt;&lt;code&gt;queryset.count()&lt;/code&gt;&lt;/a&gt; method if all you need is the count. In fact, it performs a more optimized &lt;code&gt;SELECT COUNT(*)&lt;/code&gt; at the database level.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicitly convert it to a list&lt;/strong&gt;: When you explicitly try to convert a QuerySet into a Python &lt;code&gt;list&lt;/code&gt; using the &lt;a href=&quot;https://docs.python.org/3/tutorial/datastructures.html#more-on-lists&quot;&gt;&lt;code&gt;list()&lt;/code&gt;&lt;/a&gt; constructor, you are signaling that you need all the results of that query available in memory as a standard Python list. This forces Django to execute the database query, retrieve all matching rows, instantiate them as model instances (or dictionaries/tuples), and populate the list with these items.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Call methods that return a single value or object&lt;/strong&gt;:
Many QuerySet methods are designed to return a specific piece of information or a single object, rather than a collection.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;count()&lt;/code&gt;: To return the total number of matching records, the database must be queried to perform the count.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/#django.db.models.query.QuerySet.exists&quot;&gt;&lt;code&gt;exists()&lt;/code&gt;&lt;/a&gt;: To determine if at least one matching record exists, a query must be sent to the database (though it&amp;#39;s highly optimized to stop at the first find).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/#first&quot;&gt;&lt;code&gt;first()&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/#last&quot;&gt;&lt;code&gt;last()&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/#earliest&quot;&gt;&lt;code&gt;earliest()&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/#latest&quot;&gt;&lt;code&gt;latest()&lt;/code&gt;&lt;/a&gt;: These methods are designed to retrieve a single specific object from the ordered QuerySet. This requires a database query to find and fetch that particular record.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get()&lt;/code&gt;: This method is used to retrieve a single, unique object matching the given lookup parameters. It inherently requires a database query. If no object or multiple objects are found, it raises an exception, which also implies the query was executed.&lt;/li&gt;
&lt;li&gt;Methods like &lt;a href=&quot;https://docs.djangoproject.com/en/5.2/ref/models/querysets/#django.db.models.query.QuerySet.aggregate&quot;&gt;&lt;code&gt;aggregate()&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;earliest()&lt;/code&gt;, &lt;code&gt;latest()&lt;/code&gt; also trigger immediate database queries because they compute a result based on the entire QuerySet.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This lazy evaluation is beneficial on the side of database performance because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It allows you to chain multiple filters and operations efficiently. Django can optimize the entire chain into a single, more efficient SQL query.&lt;/li&gt;
&lt;li&gt;You can create and pass QuerySets around in your code without incurring unnecessary database hits until the data is actually needed.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why Use Django QuerySets?&lt;/h3&gt;
&lt;p&gt;QuerySets are a fundamental part of Django&amp;#39;s ORM for several reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pythonic interface&lt;/strong&gt;: They provide a clean, intuitive, and Pythonic way to interact with your database. This makes your data access code readable, maintainable, and easier to understand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Abstraction over SQL&lt;/strong&gt;: QuerySets abstract away the complexities and variations of SQL syntax across different database backends. Django handles the translation from your Python QuerySet methods to the appropriate SQL for your configured database. This also provides a significant degree of database portability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: By default, QuerySets protect against SQL injection attacks because parameters are escaped correctly by the database driver. Writing raw SQL queries manually can be error-prone and open up security vulnerabilities if not handled with extreme care.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer productivity&lt;/strong&gt;: They significantly speed up development by reducing the amount of boilerplate code needed for common database operations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in features for optimization&lt;/strong&gt;: As you are about to see, Django&amp;#39;s ORM and QuerySets come with several built-in features and methods designed to help you write more performant database queries.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Implementing Basic QuerySet Performance Techniques&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s implement some basic, yet impactful, performance techniques for Django QuerySets.&lt;/p&gt;
&lt;h3&gt;Requirements, Dependencies, and Settings&lt;/h3&gt;
&lt;p&gt;To reproduce this tutorial, you need to have &lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.6 or newer installed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Suppose you call the main folder of your project &lt;code&gt;django_query_sets/&lt;/code&gt;. At the end of this step, the folder will have the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;django_query_sets/
     └── venv/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;venv/&lt;/code&gt; contains the virtual environment. You can create the &lt;code&gt;venv/&lt;/code&gt; &lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;virtual environment&lt;/a&gt; directory like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;python -m venv venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To activate it, run this on Windows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On macOS and Linux, execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the activated virtual environment, install the dependencies with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pip install django
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will &lt;a href=&quot;https://docs.djangoproject.com/en/5.2/topics/install/&quot;&gt;install the latest version of Django&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Create your Django project as &lt;code&gt;query_sets_project/&lt;/code&gt; in the &lt;code&gt;django_query_sets/&lt;/code&gt; folder type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;django-admin startproject query_sets_project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your folder now has the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;django_query_sets/
     ├── query_sets_project
     │     ├── query_sets_project
     │     │     ├── __init__.py
     │     │     ├── settings.py
     │     │     ├── urls.py
     │     │     ├── asgi.py
     │     │     └── wsgi.py
     │     └── manage.py
     │
     └── venv/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;query_sets_project/&lt;/code&gt; folder contains the main project files. In this tutorial, you will create an application that you can call &lt;em&gt;catalog&lt;/em&gt;. To do so, from &lt;code&gt;django_query_sets/&lt;/code&gt; navigate to &lt;code&gt;query_sets_project/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;cd query_sets_project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, you can instantiate the &lt;code&gt;catalog/&lt;/code&gt; folder:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;python manage.py startapp catalog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The whole project now has the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;django_query_sets/
    ├── query_sets_project/
    │        ├── manage.py
    │        ├── query_sets_project/
    │        │      ├── __init__.py
    │        │      ├── asgi.py
    │        │      ├── settings.py
    │        │      ├── urls.py
    │        │      └── wsgi.py
    │        └── catalog/
    │               ├── __init__.py
    │               ├── admin.py
    │               ├── apps.py
    │               ├── models.py
    │               ├── tests.py
    │               ├── views.py
    │               └── migrations/
    │                     └── __init__.py
    └── venv/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go to the &lt;code&gt;django_query_sets/query_sets_project/query_sets_project/settings.py&lt;/code&gt; file. Inside it, there is a list of installed apps which appears as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;INSTALLED_APPS = [
    &amp;quot;django.contrib.admin&amp;quot;,
    &amp;quot;django.contrib.auth&amp;quot;,
    &amp;quot;django.contrib.contenttypes&amp;quot;,
    &amp;quot;django.contrib.sessions&amp;quot;,
    &amp;quot;django.contrib.messages&amp;quot;,
    &amp;quot;django.contrib.staticfiles&amp;quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You need to add your catalog application to the list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;INSTALLED_APPS = [
    &amp;quot;django.contrib.admin&amp;quot;,
    &amp;quot;django.contrib.auth&amp;quot;,
    &amp;quot;django.contrib.contenttypes&amp;quot;,
    &amp;quot;django.contrib.sessions&amp;quot;,
    &amp;quot;django.contrib.messages&amp;quot;,
    &amp;quot;django.contrib.staticfiles&amp;quot;,
    &amp;#39;catalog.apps.CatalogConfig&amp;#39;, # Add catalog app to the list
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Perfect! You are now ready to start writing Django code and using QuerySets!&lt;/p&gt;
&lt;h3&gt;Setting up a Sample Model&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s define a couple of Django models in &lt;code&gt;catalog/models.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;Author&lt;/code&gt; and &lt;code&gt;Book&lt;/code&gt; models, establishing a one-to-many relationship:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.db import models
from django.utils import timezone

class Author(models.Model):
    name = models.CharField(max_length=100, unique=True)
    bio = models.TextField(blank=True, null=True) # Author biography

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    # related_name=&amp;#39;books&amp;#39; allows us to do author_instance.books.all()
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name=&amp;#39;books&amp;#39;)
    publication_date = models.DateField()
    pages = models.IntegerField()
    price = models.DecimalField(max_digits=6, decimal_places=2)
    # ISBN is unique, but can be nullable if not always available
    isbn = models.CharField(max_length=13, unique=True, null=True, blank=True)

    class Meta:
        ordering = [&amp;#39;title&amp;#39;] # Default ordering for Book queries

    def __str__(self):
        return self.title
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After defining or modifying your models, you must create and apply database migrations (via the CLI):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;python manage.py makemigrations catalog
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Below is the result:
&lt;img src=&quot;/images/blog/2025-12/migrations.png&quot; alt=&quot;Applying migrations to a Django project&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This updates your database schema to reflect your model definitions.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;Now, populate the database with some sample data. You can do this via the Django admin interface (after registering your models in &lt;code&gt;admin.py&lt;/code&gt;) or programmatically, using the Django shell (&lt;code&gt;python manage.py shell&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;In this case, we will use the Python shell. Inside the &lt;code&gt;django_query_sets/query_sets_project/&lt;/code&gt; folder, type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;python manage.py shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Paste the following code into the CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from catalog.models import Author, Book
from django.utils import timezone
import datetime

# Create an author
author1 = Author.objects.create(name=&amp;quot;George Orwell&amp;quot;)
author2 = Author.objects.create(name=&amp;quot;J.R.R. Tolkien&amp;quot;)

# Create some books
Book.objects.create(
    title=&amp;quot;1984&amp;quot;,
    author=author1,
    publication_date=datetime.date(1949, 6, 8),
    pages=328,
    price=15.99,
    isbn=&amp;quot;9780451524935&amp;quot;
)
Book.objects.create(
    title=&amp;quot;Animal Farm&amp;quot;,
    author=author1,
    publication_date=datetime.date(1945, 8, 17),
    pages=112,
    price=12.50,
    isbn=&amp;quot;9780451526342&amp;quot;
)
Book.objects.create(
    title=&amp;quot;The Hobbit&amp;quot;,
    author=author2,
    publication_date=datetime.date(1937, 9, 21),
    pages=310,
    price=14.00,
    isbn=&amp;quot;9780547928227&amp;quot;
)
Book.objects.create(
    title=&amp;quot;The Lord of the Rings&amp;quot;,
    author=author2,
    publication_date=datetime.date(1954, 7, 29),
    pages=1178,
    price=25.00,
    isbn=&amp;quot;9780618640157&amp;quot;
)

print(&amp;quot;Sample data created successfully!&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When done, type &lt;code&gt;exit()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The data you have created will now be persisted in your database. You are ready to query it using QuerySets!&lt;/p&gt;
&lt;h3&gt;Fetching Specific Fields with &lt;code&gt;values()&lt;/code&gt; and &lt;code&gt;values_list()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;By default, when you retrieve objects from a database, Django fetches all fields for each object. If you only need a subset of these fields, fetching everything is wasteful. This is where &lt;code&gt;values()&lt;/code&gt; and &lt;code&gt;values_list()&lt;/code&gt; come in handy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;values(*fields)&lt;/code&gt;&lt;/strong&gt;: Returns a QuerySet that returns dictionaries, rather than model instances, when used as an iterable. Each dictionary represents an object, with the keys corresponding to the field names you specified.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;values_list(*fields, flat=False)&lt;/code&gt;&lt;/strong&gt;: Similar to &lt;code&gt;values()&lt;/code&gt;, but it returns tuples instead of dictionaries. If you only specify one field, you can use &lt;code&gt;flat=True&lt;/code&gt; to get a flat list of single values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is more performant because fetching only specific fields reduces the amount of data transferred from the database to your application and decreases the memory required to hold the results. It also avoids the overhead of creating full model instances if you do not need them.&lt;/p&gt;
&lt;p&gt;For example, say you only need the titles and publication dates of all books. In &lt;code&gt;catalog/views.py&lt;/code&gt;, write the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.shortcuts import render
from django.http import HttpResponse
from .models import Book

def book_titles_and_dates_view(request):
    # Using values()
    book_data_dicts = Book.objects.values(&amp;#39;title&amp;#39;, &amp;#39;publication_date&amp;#39;)

    output_lines = [&amp;quot;&amp;lt;h3&amp;gt;Book Titles and Publication Dates (using values()):&amp;lt;/h3&amp;gt;&amp;quot;]
    for book_dict in book_data_dicts:
        # Accessing data by dictionary keys
        output_lines.append(f&amp;quot;Title: {book_dict[&amp;#39;title&amp;#39;]}, Published: {book_dict[&amp;#39;publication_date&amp;#39;]}&amp;quot;)
    return HttpResponse(&amp;quot;&amp;lt;br&amp;gt;&amp;quot;.join(output_lines))

def book_titles_only_view(request):
    # Using values_list()
    book_titles = Book.objects.values_list(&amp;#39;title&amp;#39;, flat=True)

    output_lines = [&amp;quot;&amp;lt;h3&amp;gt;Book Titles Only (using values_list(flat=True)):&amp;lt;/h3&amp;gt;&amp;quot;]
    # Iterating over a flat list of titles
    for title in book_titles:
        output_lines.append(title)

    # Print query to console
    print(&amp;quot;--- Book Titles (from values_list with flat=True) ---&amp;quot;)
    for title in book_titles:
        print(title)
    print(&amp;quot;----------------------------------------------------&amp;quot;)
    print(str(book_titles.query))

    return HttpResponse(&amp;quot;&amp;lt;br&amp;gt;&amp;quot;.join(output_lines))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see these views in action, you need to map them to URLs. Create a &lt;code&gt;catalog/urls.py&lt;/code&gt; file and write the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.urls import path
from . import views

app_name = &amp;#39;catalog&amp;#39; # Optional but good practice for namespacing

urlpatterns = [
    path(&amp;#39;titles-dates/&amp;#39;, views.book_titles_and_dates_view, name=&amp;#39;book_titles_dates&amp;#39;),
    path(&amp;#39;titles-only/&amp;#39;, views.book_titles_only_view, name=&amp;#39;book_titles_only&amp;#39;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, modify &lt;code&gt;query_sets_project/query_sets_project/urls.py&lt;/code&gt; to include the &lt;code&gt;catalog/urls.py&lt;/code&gt; file. The code you will find in &lt;code&gt;query_sets_project/query_sets_project/urls.py&lt;/code&gt; is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib import admin
from django.urls import path

urlpatterns = [
    path(&amp;quot;admin/&amp;quot;, admin.site.urls),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Change it as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path(&amp;#39;admin/&amp;#39;, admin.site.urls),
    path(&amp;#39;catalog/&amp;#39;, include(&amp;#39;catalog.urls&amp;#39;)),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see the results, navigate to your project&amp;#39;s root directory (&lt;code&gt;django_query_sets/query_sets_project/&lt;/code&gt;) in your terminal and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see the results, go to the dedicated URLs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;http://127.0.0.1:8000/catalog/titles-dates/&quot;&gt;http://127.0.0.1:8000/catalog/titles-dates/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/dates.png&quot; alt=&quot;Managing titles dates in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;http://127.0.0.1:8000/catalog/titles-only/&quot;&gt;http://127.0.0.1:8000/catalog/titles-only/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/titles.png&quot; alt=&quot;Managing titles in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the &lt;code&gt;titles-only&lt;/code&gt; views run, you can see the resulting query via the CLI:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/titles_optimized_query.png&quot; alt=&quot;Optimized query in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Now, to see the difference in performance with the &amp;quot;standard method&amp;quot;, you can add this method in &lt;code&gt;catalog/views.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def all_book_details_view(request):
    # This fetches ALL fields for every book from the database.
    all_books = Book.objects.all()

    # Print SQL query
    print(str(all_books.query))

    output_lines = [&amp;quot;&amp;lt;h3&amp;gt;All Book Details (Default Method):&amp;lt;/h3&amp;gt;&amp;quot;]
    for book in all_books:
        output_lines.append(book.title)

    return HttpResponse(&amp;quot;&amp;lt;br&amp;gt;&amp;quot;.join(output_lines))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database is doing the work of retrieving every column, and Django is building a full model instance for each row, only for us to use a single field: &lt;code&gt;title&lt;/code&gt;. This is the inefficiency eliminated by &lt;code&gt;values()&lt;/code&gt; and &lt;code&gt;values_list()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;catalog/urls.py&lt;/code&gt;, add a dedicated URL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;urlpatterns = [
    # Existing code not shown for brevity
    path(&amp;#39;all-details/&amp;#39;, views.all_book_details_view, name=&amp;#39;all_book_details&amp;#39;), # Add this line
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rerun the server and go to &lt;code&gt;http://127.0.0.1:8000/catalog/all-details/&lt;/code&gt;. The result you see in the UI is the same as at &lt;code&gt;http://127.0.0.1:8000/catalog/titles-only/&lt;/code&gt;. What changes is the query under the hood. You can see it printed in the command line:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/titles_query_inefficient.png&quot; alt=&quot;Optimized query in Django with QuerySets inefficient titles&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This shows that you are obtaining the same result (listed titles), but with a longer and inefficient query.&lt;/p&gt;
&lt;h3&gt;Efficient Counting with &lt;code&gt;count()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Often, you just need to know how many objects match a particular query, not the objects themselves. A naive approach might be to fetch all objects and then use &lt;code&gt;len()&lt;/code&gt; on the resulting list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;all_books = Book.objects.all()
number_of_books = len(all_books) # This evaluates the QuerySet and loads all book objects
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is inefficient because it loads all the book objects into memory just to count them. Django QuerySets provides a much better way: the &lt;code&gt;count()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;It performs a &lt;code&gt;SELECT COUNT(*)&lt;/code&gt; (or a similar count-specific query) at the database level. This is significantly faster and uses far less memory than retrieving all objects.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s add a view to demonstrate this. Add the following as a new function in &lt;code&gt;catalog/views.py&lt;/code&gt; (do not cancel the others):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from .models import Author # Add this import on top of the file
from django.db import connection # Add this import on top of the file

def book_count_view(request):
    # Total count query
    total_books = Book.objects.count()

    print(&amp;quot;\n--- Actual query for total book count ---&amp;quot;)
    print(connection.queries[0][&amp;#39;sql&amp;#39;])
    print(&amp;quot;-----------------------------------------&amp;quot;)

    # Filtered count query
    connection.queries.clear()
    orwell_books_count = Book.objects.filter(author__name=&amp;quot;George Orwell&amp;quot;).count()

    print(&amp;quot;\n--- Actual query for filtered book count ---&amp;quot;)
    print(connection.queries[0][&amp;#39;sql&amp;#39;])
    print(&amp;quot;--------------------------------------------&amp;quot;)

    output_lines = [
        &amp;quot;&amp;lt;h3&amp;gt;Book Counts (using count()):&amp;lt;/h3&amp;gt;&amp;quot;,
        f&amp;quot;Total books in the library: {total_books}&amp;quot;,
        f&amp;quot;Number of books by George Orwell: {orwell_books_count}&amp;quot;
    ]
    return HttpResponse(&amp;quot;&amp;lt;br&amp;gt;&amp;quot;.join(output_lines))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now add a dedicated URL in &lt;code&gt;catalog/urls.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.urls import path
from . import views

app_name = &amp;#39;catalog&amp;#39;

urlpatterns = [
    # Existing code not shown for brevity
    path(&amp;#39;book-counts/&amp;#39;, views.book_count_view, name=&amp;#39;book_counts&amp;#39;), # Add this line
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is shown at &lt;a href=&quot;http://127.0.0.1:8000/catalog/book-counts/&quot;&gt;http://127.0.0.1:8000/catalog/book-counts/&lt;/a&gt;, after re-running the server:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/count.png&quot; alt=&quot;Counting in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The console shows the underlying query:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/count_queries_optimized.png&quot; alt=&quot;Counting optimized in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;p&gt;As before, you can create a function that performs the same query (but inefficiently) in &lt;code&gt;views.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def book_count_slow_view(request):
    # Fetch all book objects from the database
    all_books = Book.objects.all()

    print(&amp;quot;\n--- Query for book_count_slow_view ---&amp;quot;)
    print(str(all_books.query))
    print(&amp;quot;--------------------------------------&amp;quot;)

    # Use len() for loading all objects into memory
    number_of_books = len(all_books)

    output_lines = [
        &amp;quot;&amp;lt;h3&amp;gt;Book Count (Slow Method):&amp;lt;/h3&amp;gt;&amp;quot;,
        f&amp;quot;Total books in the library: {number_of_books}&amp;quot;
    ]
    return HttpResponse(&amp;quot;&amp;lt;br&amp;gt;&amp;quot;.join(output_lines))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the URL to &lt;code&gt;urls.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;urlpatterns = [
    # Existing code not shown for brevity
    path(&amp;#39;book-counts-slow/&amp;#39;, views.book_count_slow_view, name=&amp;#39;book_counts_slow&amp;#39;), # Add this line
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rerun the server and go to &lt;code&gt;http://127.0.0.1:8000/catalog/book-counts-slow/&lt;/code&gt;. You will see the same result in the UI as the one in &lt;code&gt;http://127.0.0.1:8000/catalog/book-counts/&lt;/code&gt;. Again, what changes is the query under the hood. You can see it printed in the command line:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/count_queries_inefficient.png&quot; alt=&quot;Counting in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The image shows how inefficient this query is, as it retrieves all the data from the database.&lt;/p&gt;
&lt;h3&gt;Efficient Existence Checks with &lt;code&gt;exists()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Sometimes, you only need to know whether at least one object matches your query. For example: &amp;quot;Are there any books by J.R.R. Tolkien in the database?&amp;quot;&lt;/p&gt;
&lt;p&gt;To do so efficiently, create a view by adding a new function in &lt;code&gt;catalog/views.py&lt;/code&gt; that uses the method &lt;code&gt;exists()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def check_books_exist_view(request):
    # Use the efficient method exists()
    connection.queries.clear()
    tolkien_books_exist = Book.objects.filter(author__name=&amp;quot;J.R.R. Tolkien&amp;quot;).exists()

    print(&amp;quot;\n--- Actual query for exists() check ---&amp;quot;)
    print(connection.queries[0][&amp;#39;sql&amp;#39;])
    print(&amp;quot;---------------------------------------&amp;quot;)

    output_lines = [&amp;quot;&amp;lt;h3&amp;gt;Book Existence Checks (using exists()):&amp;lt;/h3&amp;gt;&amp;quot;]
    if tolkien_books_exist:
        output_lines.append(&amp;quot;Yes, there are books by J.R.R. Tolkien in the database.&amp;quot;)
    else:
        output_lines.append(&amp;quot;No books by J.R.R. Tolkien found.&amp;quot;)

    return HttpResponse(&amp;quot;&amp;lt;br&amp;gt;&amp;quot;.join(output_lines))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As before, add a new URL in &lt;code&gt;catalog/urls.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;urlpatterns = [
    # Existing code not shown for brevity
    path(&amp;#39;check-existence/&amp;#39;, views.check_books_exist_view, name=&amp;#39;check_book_existence&amp;#39;), # Add this line
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After re-running the server, the result is visible at &lt;a href=&quot;http://127.0.0.1:8000/catalog/check-existence/&quot;&gt;http://127.0.0.1:8000/catalog/check-existence/&lt;/a&gt; :&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/exists.png&quot; alt=&quot;Using exists in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The console shows the underlying query:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/exists_query.png&quot; alt=&quot;Using exists query in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;p&gt;To obtain the same result, but inefficiently, you could create such a function in &lt;code&gt;views.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def check_books_exist_slow_view(request):
    output_lines = [&amp;quot;&amp;lt;h3&amp;gt;Book Existence Checks (Slow Methods):&amp;lt;/h3&amp;gt;&amp;quot;]

    # Inefficient method 1: Using count() &amp;gt; 0
    connection.queries.clear()
    # This asks the database to find and count ALL matching books.
    if Book.objects.filter(author__name=&amp;quot;J.R.R. Tolkien&amp;quot;).count() &amp;gt; 0:
        output_lines.append(&amp;quot;Using count(): Yes, books by J.R.R. Tolkien exist.&amp;quot;)

    print(&amp;quot;\n--- Actual query for count() existence check ---&amp;quot;)
    print(connection.queries[0][&amp;#39;sql&amp;#39;])
    print(&amp;quot;------------------------------------------------&amp;quot;)

    # Inefficient method 2: Evaluating the QuerySet directly
    connection.queries.clear()
    # This fetches ALL matching books into memory.
    if Book.objects.filter(author__name=&amp;quot;J.R.R. Tolkien&amp;quot;):
        output_lines.append(&amp;quot;Using boolean check: Yes, books by J.R.R. Tolkien exist.&amp;quot;)

    print(&amp;quot;\n--- Actual query for boolean existence check ---&amp;quot;)
    print(connection.queries[0][&amp;#39;sql&amp;#39;])
    print(&amp;quot;------------------------------------------------&amp;quot;)

    return HttpResponse(&amp;quot;&amp;lt;br&amp;gt;&amp;quot;.join(output_lines))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new URL in &lt;code&gt;urls.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;urlpatterns = [
     # Existing code not shown for brevity
    path(&amp;#39;check-existence-slow/&amp;#39;, views.check_books_exist_slow_view, name=&amp;#39;check_book_existence_slow&amp;#39;) # Add this line
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When checking &lt;code&gt;http://127.0.0.1:8000/catalog/check-existence-slow/&lt;/code&gt;, you will see this result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/exists_inefficient.png&quot; alt=&quot;Using inefficient exists in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Again, what changes is the query:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-12/exists_inefficient_query.png&quot; alt=&quot;Using inefficient exists query in Django with QuerySets&quot;/&gt;&lt;/p&gt;
&lt;p&gt;In this case, one query counts to the end, and the other fetches too much data. Instead, when using &lt;code&gt;exists()&lt;/code&gt;, the optimization of SQL contains the &lt;code&gt;LIMIT 1&lt;/code&gt; clause, which is the database&amp;#39;s instruction to perform the absolute minimum work required.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Side-note&lt;/strong&gt;: While Django&amp;#39;s ORM is powerful, it&amp;#39;s important to be aware of potential pitfalls like the N+1 query problem, which can significantly degrade performance when fetching related objects. For a detailed guide on identifying and fixing these issues with AppSignal, check out &lt;a href=&quot;https://blog.appsignal.com/2024/12/04/find-and-fix-n-plus-one-queries-in-django-using-appsignal.html&quot;&gt;Find and Fix N+1 Queries in Django Using AppSignal&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we explored key techniques for optimizing Django QuerySets. These methods reduce data transfer, minimize memory usage, and lessen the load on your database server, leading to a faster, more responsive, and scalable web application. Mastering these is a crucial step for any Django developer aiming to build high-performance systems.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Switching from Pip to uv in Python: A Comprehensive Guide</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/09/24/switching-from-pip-to-uv-in-python-a-comprehensive-guide.html"/>
    <id>https://blog.appsignal.com/2025/09/24/switching-from-pip-to-uv-in-python-a-comprehensive-guide.html</id>
    <published>2025-09-24T00:00:00+00:00</published>
    <updated>2025-09-24T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Learn about the ins and outs of uv and how to transition from Pip to uv.</summary>
    <content type="html">&lt;p&gt;Python has long relied on &lt;code&gt;pip&lt;/code&gt; as its standard package manager, but a
blazing-fast alternative is now changing the landscape.&lt;/p&gt;
&lt;p&gt;uv is a Rust-based package
manager that aims to transform Python dependency management with unmatched
performance and simplicity.&lt;/p&gt;
&lt;p&gt;In this guide, you will learn everything you need to know about uv and how to
smoothly transition from the traditional pip ecosystem.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get started!&lt;/p&gt;
&lt;h2&gt;What is uv?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt; is a high-performance Python package and project manager written in Rust. It
was developed by Astral (the company behind the
&lt;a href=&quot;https://github.com/astral-sh/ruff&quot;&gt;popular Ruff linter&lt;/a&gt;), and is designed to be
the comprehensive solution for Python package management.&lt;/p&gt;
&lt;p&gt;With uv, you can replace a wide range of traditional tools in your workflow, such
as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip&lt;/code&gt; for installing packages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pip-tools&lt;/code&gt; for dependency locking.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virtualenv&lt;/code&gt;/&lt;code&gt;venv&lt;/code&gt; for environment management.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pyenv&lt;/code&gt; for managing Python versions.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipx&lt;/code&gt; for installing CLI tools.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;poetry&lt;/code&gt; for project configuration and publishing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;twine&lt;/code&gt; for distributing packages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks to its Rust-based roots, uv performs operations 10-100 times faster than
these traditional Python tools, and this increase in speed is noticeable even in small
projects.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-09/7.png&quot; alt=&quot;Benchamrking uv against standard Python tools&quot;/&gt;&lt;/p&gt;
&lt;p&gt;uv also embraces modern packaging standards. It uses &lt;code&gt;pyproject.toml&lt;/code&gt; for
configuration and introduces a reliable lock file that ensures consistent
environments across systems.&lt;/p&gt;
&lt;p&gt;Before we explore uv in detail, let&amp;#39;s quickly recap the current Python landscape
for package management.&lt;/p&gt;
&lt;h3&gt;Traditional Python Package Approach: &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;virtualenv&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The traditional Python package management workflow relies on a fragmented set of
tools, each handling a specific piece of the puzzle.&lt;/p&gt;
&lt;p&gt;It typically begins with installing Python, either via system package managers
or by downloading it from the official website.&lt;/p&gt;
&lt;p&gt;From there, you might use tools like &lt;code&gt;venv&lt;/code&gt; or &lt;code&gt;virtualenv&lt;/code&gt; to create isolated
environments, followed by &lt;code&gt;pip&lt;/code&gt; to install packages. Dependency tracking is
often manual or handled through add-ons like &lt;code&gt;pip-tools&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A typical workflow with &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;virtualenv&lt;/code&gt; might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Create a virtual environment
python -m venv .venv

# Activate the environment
source .venv/bin/activate

# Install packages
pip install django requests

# Record dependencies
pip freeze &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this workflow is well-known and widely used, it involves multiple steps
and tools, each with its own syntax and quirks. You must remember to activate
environments, manage dependencies manually, and juggle a variety of commands.&lt;/p&gt;
&lt;p&gt;This is precisely the pain point uv aims to solve by streamlining the entire
process into a faster and more cohesive experience.&lt;/p&gt;
&lt;h2&gt;Installing uv&lt;/h2&gt;
&lt;p&gt;Getting started with uv is quick and straightforward, with support across all
major operating systems. You can follow the
&lt;a href=&quot;https://docs.astral.sh/uv/getting-started/installation/&quot;&gt;official uv installation guide&lt;/a&gt;,
or use one of the commands below.&lt;/p&gt;
&lt;p&gt;On Linux and macOS, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl -LsSf https://astral.sh/uv/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Windows, use PowerShell:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;powershell -ExecutionPolicy ByPass -c &amp;quot;irm https://astral.sh/uv/install.ps1 | iex&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After installation, confirm that it&amp;#39;s working by checking the version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv --version
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv 0.7.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To update uv later, simply run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv self update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will fetch and install the latest version automatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;info: Checking for updates...
success: Upgraded uv from v0.6.13 to v0.7.3! https://github.com/astral-sh/uv/releases/tag/0.7.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The easiest way to begin using uv is to replace the tools you already rely on.
Since uv is designed as a drop-in replacement, you can take advantage of its
performance and simplicity without overhauling your current workflow.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s look at how that works in practice.&lt;/p&gt;
&lt;h2&gt;Managing Virtual Environments with uv for Python&lt;/h2&gt;
&lt;p&gt;uv can serve as a direct replacement for tools like &lt;code&gt;virtualenv&lt;/code&gt; and &lt;code&gt;venv&lt;/code&gt; to
create isolated Python environments.&lt;/p&gt;
&lt;p&gt;Previously, you might run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;python -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With uv, the equivalent is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv venv # uses .venv by default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, this creates a virtual environment in &lt;code&gt;.venv&lt;/code&gt;, but you can specify a
custom path if needed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv venv &amp;lt;path/to/environment&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running the command, uv provides a clear summary showing which Python
interpreter was used, where the environment was created, and how to activate it
in your shell:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Using CPython 3.13.2 interpreter at: /home/ayo/.local/share/mise/installs/python/3.13.2/bin/python
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate.fish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also require that a specific version of Python is used:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv venv --python 3.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the version isn&amp;#39;t already installed, uv will fetch and install it for you
automatically.&lt;/p&gt;
&lt;p&gt;In practice, uv&amp;#39;s environment creation is incredibly fast. In my tests, it was
roughly &lt;strong&gt;200x faster&lt;/strong&gt; than &lt;code&gt;venv&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-09/3.png&quot; alt=&quot;Creating virtual environments with uv is 200x faster than venv&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Replacing Pip with uv for Dependency Management&lt;/h2&gt;
&lt;p&gt;uv provides a &lt;code&gt;pip&lt;/code&gt;-compatible interface for managing your project dependencies.
You only need to prefix specific &lt;code&gt;pip&lt;/code&gt; commands with &lt;code&gt;uv&lt;/code&gt;. For example, you can
install dependencies with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv pip install flask requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-09/1.png&quot; alt=&quot;Installing packages with Pip&quot;/&gt;&lt;/p&gt;
&lt;p&gt;There&amp;#39;s no need to activate a virtual environment beforehand, as uv automatically
detects and uses the nearest environment in your current or parent directory.&lt;/p&gt;
&lt;p&gt;If no environment is found, it will prompt you to create one or suggest using
the &lt;code&gt;--system&lt;/code&gt; flag to install it globally:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-09/2.png&quot; alt=&quot;uv pip install fails without a virtual environment&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Installing dependencies from existing project files works the same way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv pip install -r pyproject.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&amp;#39;re using &lt;a href=&quot;https://github.com/jazzband/pip-tools&quot;&gt;pip-tools&lt;/a&gt; to compile
&lt;code&gt;requirements.txt&lt;/code&gt; from a &lt;code&gt;requirements.in&lt;/code&gt; file, uv provides a much faster
alternative:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip-compile requirements.in -o requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the uv equivalent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv pip compile requirements.in -o requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I experienced a &lt;strong&gt;~150x&lt;/strong&gt; speedup over &lt;code&gt;pip-compile&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-09/4.png&quot; alt=&quot;uv compile is faster than pip-compile&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Removing dependencies is also straightforward with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv pip uninstall flask
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Managing Python Versions with uv&lt;/h2&gt;
&lt;p&gt;One of uv&amp;#39;s key advantages is that it doesn&amp;#39;t rely on Python to run. Instead, it
automatically detects existing Python installations and uses them for any
Python-specific tasks.&lt;/p&gt;
&lt;p&gt;If a project requires a version of Python that isn&amp;#39;t already installed, uv will
also download and manage it for you.&lt;/p&gt;
&lt;p&gt;You can install Python versions directly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv python install # the latest version
uv python install 3.13 # a specific version
uv python install 3.13 3.12 # multiple versions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To check which versions are available on your system, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv python list --only-installed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lists all Python versions installed by uv, as well as any others already on
your system, regardless of how they were installed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cpython-3.13.2-linux-x86_64-gnu     /home/ayo/.local/share/mise/installs/python/3.13.2/bin/python3.13
cpython-3.13.2-linux-x86_64-gnu     /home/ayo/.local/share/mise/installs/python/3.13.2/bin/python3 -&amp;gt; python3.13
cpython-3.13.2-linux-x86_64-gnu     /home/ayo/.local/share/mise/installs/python/3.13.2/bin/python -&amp;gt; python3.13
cpython-3.12.10-linux-x86_64-gnu    /home/ayo/.local/share/uv/python/cpython-3.12.10-linux-x86_64-gnu/bin/python3.12
cpython-3.12.3-linux-x86_64-gnu     /usr/bin/python3.12
cpython-3.12.3-linux-x86_64-gnu     /usr/bin/python3 -&amp;gt; python3.12
cpython-3.11.12-linux-x86_64-gnu    /home/ayo/.local/share/uv/python/cpython-3.11.12-linux-x86_64-gnu/bin/python3.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With uv, you don&amp;#39;t need to install Python ahead of time. If a command requires a
version you don&amp;#39;t yet have, uv will fetch and install it automatically before
proceeding. This on-demand behavior eliminates the need for separate tools like
&lt;code&gt;pyenv&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Simplifying Tasks with uv&lt;/h2&gt;
&lt;p&gt;So far, I&amp;#39;ve focused on how uv can serve as a faster, drop-in replacement for
tools you&amp;#39;re already using, allowing you to gain performance benefits with
minimal disruption.&lt;/p&gt;
&lt;p&gt;But uv also introduces an alternative, more modern workflow that simplifies many
tasks and goes beyond what traditional tools offer.&lt;/p&gt;
&lt;p&gt;In the next sections, we&amp;#39;ll explore some of these new capabilities.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h2&gt;Creating and Managing Python Projects with uv&lt;/h2&gt;
&lt;p&gt;One of uv&amp;#39;s core goals is to manage the entire lifecycle of a Python project,
from creating the project itself, to managing its dependencies, running commands
or scripts, and even preparing it for distribution.&lt;/p&gt;
&lt;p&gt;To start a new project, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv init &amp;lt;project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, this sets up an application-style project. To create a library
project instead, use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv init --lib &amp;lt;project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An initialized application project has the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.
├── .git
├── .gitignore
├── main.py
├── pyproject.toml
├── .python-version
└── README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;uv sets up version control with Git, includes &lt;code&gt;pyproject.toml&lt;/code&gt; for project
metadata and dependencies, and creates a basic script (&lt;code&gt;main.py&lt;/code&gt;) along with a
&lt;code&gt;.python-version&lt;/code&gt; file. It&amp;#39;s ready to run out of the box.&lt;/p&gt;
&lt;p&gt;Execute the script with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv run main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first time you run this, uv will select the appropriate Python version,
create a virtual environment, and then execute the script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Using CPython 3.12.10
Creating virtual environment at: .venv
Hello from example-project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, uv generates a &lt;code&gt;uv.lock&lt;/code&gt; file, which records the full, resolved
dependency graph — ensuring consistent environments across different machines.&lt;/p&gt;
&lt;p&gt;While &lt;code&gt;pyproject.toml&lt;/code&gt; defines intended dependencies (like &lt;code&gt;requirements.in&lt;/code&gt;),
&lt;code&gt;uv.lock&lt;/code&gt; captures the exact installed versions, effectively replacing
&lt;code&gt;requirements.txt&lt;/code&gt; for reproducibility.&lt;/p&gt;
&lt;h2&gt;Managing Dependencies to a uv Project&lt;/h2&gt;
&lt;p&gt;It&amp;#39;s simple to add dependencies to a uv project. To add a
package, use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv add requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-09/5.png&quot; alt=&quot;Installing dependencies with uv&quot;/&gt;&lt;/p&gt;
&lt;p&gt;For development dependencies or tools, append the &lt;code&gt;--dev&lt;/code&gt; flag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv add --dev black
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These changes are reflected in your &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# pyproject.toml
. . .
dependencies = [
    &amp;quot;requests&amp;gt;=2.32.3&amp;quot;,
]

[dependency-groups]
dev = [
    &amp;quot;black&amp;gt;=25.1.0&amp;quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can create custom dependency groups using the &lt;code&gt;--group&lt;/code&gt; flag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv add --group tools black
uv add --group production gunicorn
uv add --group dev pytest # same as uv add --dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Resulting in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[dependency-groups]
dev = [
    &amp;quot;pytest&amp;gt;=8.3.5&amp;quot;,
]
production = [
    &amp;quot;gunicorn&amp;gt;=23.0.0&amp;quot;,
]
tools = [
    &amp;quot;black&amp;gt;=25.1.0&amp;quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This structure makes it easy to install only the dependencies needed for a given
environment. For example, to install just the base and production dependencies,
run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv sync --no-group dev --no-group tools --group prod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dependencies are always installed into the project&amp;#39;s virtual environment, so
there&amp;#39;s no need to activate it manually. Once installed, you can run any tool
using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv run &amp;lt;tool&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv run pytest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This finds the tool version in your project&amp;#39;s virtual environment and executes
it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-09/6.png&quot; alt=&quot;uv run pytest&quot;/&gt;&lt;/p&gt;
&lt;p&gt;uv also provides a &lt;a href=&quot;https://docs.astral.sh/uv/concepts/tools/&quot;&gt;tools interface&lt;/a&gt;
for working with tools that aren&amp;#39;t tied to a specific project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv tool run &amp;lt;tool&amp;gt; # Same as uvx &amp;lt;tool&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, the tool and its dependencies are installed in a temporary virtual
environment and executed accordingly. If you use the tool regularly, you can
install it for easier access:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv tool install &amp;lt;tool&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Upgrading and Removing Dependencies&lt;/h3&gt;
&lt;p&gt;uv provides straightforward commands for updating and removing packages as your
project evolves. Here&amp;#39;s how to upgrade a specific package to its latest version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv add --upgrade requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command updates the package and refreshes the lock file to ensure
consistent environments across your team.&lt;/p&gt;
&lt;p&gt;Removing packages is equally simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv remove requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;uv will clean up the &lt;code&gt;pyproject.toml&lt;/code&gt;, remove unneeded transitive dependencies,
and regenerate the &lt;code&gt;uv.lock&lt;/code&gt; file automatically.&lt;/p&gt;
&lt;h2&gt;Migration Guide: From Pip to uv&lt;/h2&gt;
&lt;p&gt;Switching an existing project from Pip to uv is quite straightforward. Below is
a quick reference table showing how common pip commands map to their uv equivalents:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pip Command&lt;/th&gt;
&lt;th&gt;uv Equivalent&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python -m venv .venv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv venv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a virtual environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip install package&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv add package&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installs a package and updates project files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv pip install -r requirements.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installs from requirements file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip freeze &amp;gt; requirements.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv export -o requirements.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exports dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv pip list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists installed packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip uninstall package&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv remove package&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes a package&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;If your project currently uses &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;requirements.txt&lt;/code&gt;, you can migrate it
to uv in just a few steps.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Initialize uv in the existing project directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv init .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a &lt;code&gt;pyproject.toml&lt;/code&gt; file and sets up the project structure,
without replacing any existing files.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Remove the old virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;rm -rf .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install dependencies from &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv add -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This installs the dependencies listed in your &lt;code&gt;requirements.txt&lt;/code&gt; file in a
virtual environment, and updates the &lt;code&gt;pyproject.yml&lt;/code&gt; and &lt;code&gt;uv.lock&lt;/code&gt; files
accordingly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a new uv-managed environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv sync
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verify that everything works — for example (assuming you have a Django project):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv run manage.py migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uv run manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-09/8.png&quot; alt=&quot;Using uv in a Django Project&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Now your project should be fully migrated to uv with the same
functionality as before, but now with uv&amp;#39;s performance advantages.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;By unifying the roles of multiple tools into one high-performance system, uv
eliminates much of the friction developers have long dealt with.&lt;/p&gt;
&lt;p&gt;Its speed alone is a compelling reason to adopt it, but uv&amp;#39;s streamlined workflow
and modern packaging standards make it even more appealing. For most projects,
the transition is simple and the productivity gains are immediate.&lt;/p&gt;
&lt;p&gt;uv&amp;#39;s development is a strong signal of where the broader Python ecosystem is
headed. Now is an ideal time to adopt this tool built for the future.&lt;/p&gt;
&lt;p&gt;Thanks for reading!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Scheduling Background Tasks in Python with Celery and RabbitMQ</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/08/27/scheduling-background-tasks-in-python-with-celery-and-rabbitmq.html"/>
    <id>https://blog.appsignal.com/2025/08/27/scheduling-background-tasks-in-python-with-celery-and-rabbitmq.html</id>
    <published>2025-08-27T00:00:00+00:00</published>
    <updated>2025-08-27T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll build background tasks using Celery and RabbitMQ to create a weather notification service.</summary>
    <content type="html">&lt;p&gt;It&amp;#39;s important and useful to schedule background tasks for your Python application. Tasks allow your app to perform time-based or long-running operations without blocking the main thread or slowing down the user-facing functionality of your app.&lt;/p&gt;
&lt;p&gt;Background tasks can be used for anything from running recurring jobs like data cleanup or reporting, to sending asynchronous emails or other notifications.&lt;/p&gt;
&lt;p&gt;In this article, we will build background tasks using Celery and RabbitMQ to create a weather notification service that will deliver rain alerts with Slack.&lt;/p&gt;
&lt;p&gt;But first, let&amp;#39;s briefly explore why we would want to use Celery or RabbitMQ in the first place.&lt;/p&gt;
&lt;h2&gt;Why Celery?&lt;/h2&gt;
&lt;p&gt;Celery is a widely used distributed task queue framework for Python applications. Celery handles distributing background tasks to worker processes or nodes, manages their execution reliably, and can scale to hundreds or thousands of jobs per second in production environments.&lt;/p&gt;
&lt;p&gt;Celery is especially popular in the Python ecosystem because it fits naturally with the language’s syntax, and it&amp;#39;s commonly used in frameworks like Django or Flask. Celery uses familiar concepts — decorators for tasks, Python data structures for messages — and integrates easily with Python’s logging, error handling, and testing tools. Its configuration is also Python-based, avoiding the need to learn an entirely separate domain-specific language.&lt;/p&gt;
&lt;p&gt;Lastly, Celery’s architecture is highly flexible. It works with different message brokers such as RabbitMQ (which we will use in this tutorial), Redis, or Amazon SQS, letting the developer pick a broker that best suits their workload and infrastructure.&lt;/p&gt;
&lt;h2&gt;Why RabbitMQ?&lt;/h2&gt;
&lt;p&gt;RabbitMQ is a robust, battle-tested message broker known for its reliability and flexibility. It supports complex routing patterns via exchanges and queues, offers strong delivery guarantees (including persistence and acknowledgments), and has excellent interoperability across languages and protocols (e.g., AMQP, MQTT, STOMP).&lt;/p&gt;
&lt;p&gt;Despite its strengths, like Celery, RabbitMQ can be complex to set up and maintain compared to alternatives like Redis or cloud-native solutions like AWS SQS. Also, RabbitMQ stores messages on disk by default, which can introduce latency. For simple pub/sub or fire-and-forget patterns, lighter brokers like NATS or Redis Streams might perform better and could be easier to manage.&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s move on to building our weather notification service.&lt;/p&gt;
&lt;h2&gt;What We Will Build&lt;/h2&gt;
&lt;p&gt;In this practical example, we will create a Slack rain notification service that will check the weather forecast every day at 12pm to see whether rain is expected for the next 5 days.&lt;/p&gt;
&lt;p&gt;We&amp;#39;ll need to add 3 different APIs to our project: the OpenWeatherMap API (for getting our rain data), the GitHub API (which will act as a simple JSON database to ensure we don&amp;#39;t get duplicate notifications), and the Slack API for creating messages.&lt;/p&gt;
&lt;p&gt;In the last section of this article, we will integrate Celery and RabbitMQ into our application to create the automatic 12pm daily task schedule. &lt;a href=&quot;https://github.com/daneasterman/python-celery-rabbitmq&quot;&gt;Find the full code for this tutorial on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The file structure of our Python project will look like the diagram below. &lt;code&gt;main.py&lt;/code&gt; is the main entrypoint for our app and will include the code for making the initial request to the OpenWeatherMap API. In the &lt;code&gt;apis&lt;/code&gt; folder, we have two separate files — &lt;code&gt;github_data.py&lt;/code&gt; and &lt;code&gt;slack.py&lt;/code&gt;. These handle saving data to GitHub and creating messages in Slack.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;.
├── apis
│   ├── github_data.py
│   └── slack.py
├── celerybeat-schedule
├── main.py
└── requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prerequisites and Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The steps in this project have been created on macOS running Apple&amp;#39;s M2 chip. In particular, later in the &lt;strong&gt;Implement Celery and RabbitMQ&lt;/strong&gt; section, I install and verify RabbitMQ using Homebrew for Mac. But since RabbitMQ is cross-platform, you can also use Linux and Windows. &lt;a href=&quot;https://www.rabbitmq.com/docs/download&quot;&gt;See the official RabbitMQ documentation for more details on installation using other operating systems&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;This project uses &lt;code&gt;Python 3.10.7&lt;/code&gt;, but you can also safely use any later Python version. Everything will be installed with &lt;code&gt;pip&lt;/code&gt; inside a virtual environment.&lt;/li&gt;
&lt;li&gt;You need basic knowledge of Python, experience working with APIs in Python, and some basic knowledge of terminal commands.&lt;/li&gt;
&lt;li&gt;You need knowledge of GitHub token creation and setting repository permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Initial Python Project Setup&lt;/h3&gt;
&lt;p&gt;First, let&amp;#39;s create a Python project in a new directory called &lt;code&gt;python-celery-rabbitmq&lt;/code&gt;. We will also create the core &lt;code&gt;main.py&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;mkdir python-celery-rabbitmq
cd python-celery-rabbitmq
touch main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, before we start installing our packages with pip, we need to set up our virtual environment. First run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;python -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Activate your virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;source .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can install all our required packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pip install requests celery python-dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the next section, we will add the code in &lt;code&gt;main.py&lt;/code&gt; to make requests to the OpenWeatherMap API.&lt;/p&gt;
&lt;h2&gt;Query the OpenWeatherMap API with a New Python Project&lt;/h2&gt;
&lt;p&gt;Head to the &lt;a href=&quot;https://openweathermap.org/api&quot;&gt;OpenWeatherMap API page&lt;/a&gt;, create a new account, and subscribe to the cheapest pay-as-you-go plan called: &amp;quot;One Call API 3.0&amp;quot;. The website has a number of other monthly subscription and professional-tier products below the basic &amp;quot;One Call API 3.0&amp;quot; plan. But we can safely ignore those options for the purposes of this article.&lt;/p&gt;
&lt;p&gt;Also, confusingly, the OpenWeatherMap website says that the first 1000 API calls are free for One Call API. But once you go to the Stripe payment page, it says only the first 100 calls are free. Anyway, whatever the case, even with a lot of project testing, we should stay way below this limit.&lt;/p&gt;
&lt;p&gt;Once we are subscribed, grab the API key from your dashboard, and we can start the actual coding!&lt;/p&gt;
&lt;p&gt;At this point, it&amp;#39;s important that we don&amp;#39;t expose our API key. Let&amp;#39;s add the key to our &lt;code&gt;.env&lt;/code&gt; file and also make sure the &lt;code&gt;.env&lt;/code&gt; file has been added to the project&amp;#39;s &lt;code&gt;.gitignore&lt;/code&gt; so it is not tracked in version control.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;main.py&lt;/code&gt;, add the code below (note: this will change once we implement the Celery functionality in a later section of this post):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import os
import requests
from celery import Celery
from celery.schedules import crontab
from apis.github_data import check_github_json
from dotenv import load_dotenv
load_dotenv()

def weather_task(city):
    api_key = str(os.getenv(&amp;#39;OPEN_WEATHER_MAP_API_KEY&amp;#39;))
    url = &amp;quot;https://api.openweathermap.org/data/2.5/forecast&amp;quot;
    params = {&amp;quot;q&amp;quot;: city, &amp;quot;appid&amp;quot;: api_key, &amp;quot;units&amp;quot;: &amp;quot;metric&amp;quot;}
    response = requests.get(url, params=params)

    if response.status_code != 200:
        print(f&amp;quot;Error {response.status_code}: {response.text}&amp;quot;)
        return

    data = response.json()
    summaries = []

    for entry in data[&amp;quot;list&amp;quot;]:
        if &amp;quot;12:00:00&amp;quot; in entry[&amp;quot;dt_txt&amp;quot;]:
            main = entry[&amp;quot;weather&amp;quot;][0][&amp;quot;main&amp;quot;]
            description = entry[&amp;quot;weather&amp;quot;][0][&amp;quot;description&amp;quot;]

            if &amp;quot;rain&amp;quot; in description.lower() or &amp;quot;rain&amp;quot; in main.lower() or &amp;quot;rain&amp;quot; in entry:
                date = entry[&amp;quot;dt_txt&amp;quot;].split(&amp;quot; &amp;quot;)[0]
                summaries.append({
                    &amp;quot;Date&amp;quot;: date,
                    &amp;quot;Weather Description&amp;quot;: description
                })

    if summaries:
        check_github_json(summaries)
    else:
        check_github_json(None)

if __name__ == &amp;quot;__main__&amp;quot;:
    weather_task(&amp;quot;London&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are three main things to break down in this code snippet:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;First of all, we should note that the request is using the API&amp;#39;s 5-day forecast endpoint. Since this endpoint returns multiple forecasts in three-hour increments over the 5 days, we need to reduce the amount of data returned. We can do this by just getting a &amp;quot;summary&amp;quot; for the forecast at 12pm, which simplifies things greatly and is sufficient for our purposes here. So we use an &lt;code&gt;if&lt;/code&gt; statement to only filter entries containing &lt;code&gt;12:00:00&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, we need to find any instances of rain in the API data. So we&amp;#39;ll use another three-part &lt;code&gt;if&lt;/code&gt; statement to look for any mention of rain in three possible parts of the data:
&lt;code&gt;if &amp;quot;rain&amp;quot; in description.lower() or &amp;quot;rain&amp;quot; in main.lower() or &amp;quot;rain&amp;quot; in entry&lt;/code&gt;.
If rain is indeed found, we append the date and a plain language weather description to the &lt;code&gt;summaries&lt;/code&gt; list.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lastly, we have one final, important &lt;code&gt;if&lt;/code&gt; statement. If we have some rain summary data, we will pass this through to the &lt;code&gt;check_github_json&lt;/code&gt; function. But if not, we will just pass &lt;code&gt;None&lt;/code&gt; into that function.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;#39;s see exactly what &lt;code&gt;check_github_json&lt;/code&gt; does and how it works together with the other two associated functions in the &lt;code&gt;github_data.py&lt;/code&gt; file.&lt;/p&gt;
&lt;h2&gt;Use the GitHub API to Automatically De-duplicate Notifications&lt;/h2&gt;
&lt;p&gt;As mentioned earlier, we will be using the GitHub API as a convenient/no-frills database for storing JSON data from the OpenWeatherMap API every time there is a new or unique rain forecast. By storing the data with GitHub, we can check any incoming forecasts from the OpenWeatherMap API and ensure that the new forecasts are actually unique. This will allow us to prevent any duplicate notifications from taking place on Slack.&lt;/p&gt;
&lt;p&gt;Create an empty GitHub repository called &lt;strong&gt;Weather Data&lt;/strong&gt;. Add a folder called &lt;code&gt;json&lt;/code&gt; and then create a file called &lt;code&gt;data.json&lt;/code&gt; inside that. Inside &lt;code&gt;data.json&lt;/code&gt;, add an empty array (identical to a Python list): &lt;code&gt;[]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, create your GitHub public access token for your repository and set the repository permissions.&lt;/p&gt;
&lt;p&gt;Copy the token and add it to your &lt;code&gt;.env&lt;/code&gt; file with the key &lt;code&gt;GITHUB_PERSONAL_ACCESS_TOKEN&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;GITHUB_PERSONAL_ACCESS_TOKEN=your-long-alpha-numeric-github-token-here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Back in our code editor, let&amp;#39;s add all the import statements and three functions, called &lt;code&gt;check_github_json&lt;/code&gt;, &lt;code&gt;load_github_json&lt;/code&gt;, and &lt;code&gt;update_github_json&lt;/code&gt;, in a new file called &lt;code&gt;github_data.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import os
import logging
import json
from github import Github
from datetime import datetime, timezone
from dotenv import load_dotenv
load_dotenv()
logger = logging.getLogger(__name__)

def load_github_json():
    GITHUB_PERSONAL_ACCESS_TOKEN = str(os.getenv(&amp;#39;GITHUB_PERSONAL_ACCESS_TOKEN&amp;#39;))
    github = Github(GITHUB_PERSONAL_ACCESS_TOKEN)
    try:
        repo = github.get_user().get_repo(&amp;#39;weather-data&amp;#39;)
        file = repo.get_contents(&amp;quot;json/data.json&amp;quot;)
        db_data = json.loads(file.decoded_content.decode(&amp;#39;utf-8&amp;#39;))
        return repo, file, db_data
    except Exception as e:
        logger.exception(&amp;quot;An error occurred&amp;quot;)


def check_github_json(forecast):
    if forecast is None:
        return

    repo, file, db_data = load_github_json()
    new_items = []
    is_first_run = len(db_data) == 0

    for item in forecast:
        if item not in db_data:
            print(&amp;quot;Make JSON DB entry&amp;quot;)
            db_data.append(item)
            new_items.append(item)
        else:
            print(&amp;quot;Skip, found in DB&amp;quot;)

    if new_items:
        if is_first_run:
            update_github_json(repo, file, db_data, slack_items=new_items)
        else:
            update_github_json(repo, file, db_data, slack_items=[new_items[-1]])
    else:
        print(&amp;quot;No new items to commit&amp;quot;)

def update_github_json(repo, file, updated_data, slack_items):
    bytes_data = json.dumps(updated_data).encode(&amp;#39;utf-8&amp;#39;)
    commit_msg = datetime.now(timezone.utc).strftime(&amp;quot;Update weather data - %Y-%m-%d %H:%M:%S UTC&amp;quot;)
    try:
        repo.update_file(file.path, commit_msg, bytes_data, file.sha)
        print(&amp;quot;Github data updated!&amp;quot;)
        # create_slack_message(slack_items)
    except Exception as e:
        logger.exception(&amp;quot;An error occurred&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, take a quick look at the &lt;code&gt;load_github_json&lt;/code&gt; function. The code inside this function makes the request to get our &lt;code&gt;weather_data&lt;/code&gt; repository, then we get the content of our &lt;code&gt;data.json&lt;/code&gt; file and load the file as JSON. The variables &lt;code&gt;repo&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, and &lt;code&gt;db_data&lt;/code&gt; can then be reused in &lt;code&gt;check_github_data&lt;/code&gt; and &lt;code&gt;update_github_data&lt;/code&gt; functions.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;In the &lt;code&gt;check_github_json&lt;/code&gt; function, we use a &lt;code&gt;for&lt;/code&gt; loop to iterate through the forecast data we received from the OpenWeatherMap data in &lt;code&gt;main.py&lt;/code&gt;. First, we check if the forecast item is already in our GitHub &lt;code&gt;db_data&lt;/code&gt; JSON. If it&amp;#39;s not, we append to &lt;code&gt;db_data&lt;/code&gt; and the &lt;code&gt;new_items&lt;/code&gt; variables. For the first run, we want to send all our &lt;code&gt;new_items&lt;/code&gt; to the &lt;code&gt;update_github_json&lt;/code&gt; function. On subsequent runs, we just want to pass the last item (using -1) to that function.&lt;/p&gt;
&lt;p&gt;Lastly, in &lt;code&gt;update_github_json&lt;/code&gt;, we execute the built-in &lt;code&gt;update_file&lt;/code&gt; method, which requires a commit message like all changes to a GitHub repository. Here, we create a unique commit message each time by recording the exact time and date of the commit using Python&amp;#39;s datetime module.&lt;/p&gt;
&lt;p&gt;At this point, let&amp;#39;s check that everything is working as intended by running &lt;code&gt;python main.py&lt;/code&gt; in our project root.&lt;/p&gt;
&lt;p&gt;If we are successful, first, we should see something similar to this printed in our terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;Make JSON DB entry
Make JSON DB entry
Make JSON DB entry
Github data updated!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is already a pretty good sign that things have worked correctly! Now, go to your GitHub repository and click on the Raw button to see your data more clearly. Your empty array should be replaced with the new weather data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
  {
    &amp;quot;Date&amp;quot;: &amp;quot;2025-06-05&amp;quot;,
    &amp;quot;Weather Description&amp;quot;: &amp;quot;light rain&amp;quot;
  },
  {
    &amp;quot;Date&amp;quot;: &amp;quot;2025-06-07&amp;quot;,
    &amp;quot;Weather Description&amp;quot;: &amp;quot;light rain&amp;quot;
  },
  {
    &amp;quot;Date&amp;quot;: &amp;quot;2025-06-08&amp;quot;,
    &amp;quot;Weather Description&amp;quot;: &amp;quot;light rain&amp;quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our next section on the Slack API, we will implement code to fire off a notification once we get a new and unique rain forecast.&lt;/p&gt;
&lt;h2&gt;Create Message Notifications with the Slack API&lt;/h2&gt;
&lt;p&gt;In this section, we will use the Slack WebClient SDK.&lt;/p&gt;
&lt;p&gt;First, let&amp;#39;s go to the Slack API page to get our &amp;quot;Bot&amp;quot; credentials so we can implement our notifications feature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to &lt;a href=&quot;https://api.slack.com/apps&quot;&gt;https://api.slack.com/apps&lt;/a&gt;, click on &lt;strong&gt;Create New App&lt;/strong&gt; and select &lt;strong&gt;From Scratch&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the next modal window, give your app a name and select the workspace you want to deploy it to (it&amp;#39;s a good idea to create your own private workspace dedicated to this project).&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;OAuth &amp;amp; Permissions&lt;/strong&gt;, and then under &lt;strong&gt;Bot Token Scopes&lt;/strong&gt;, update the following three permissions: &lt;code&gt;channels:join&lt;/code&gt;, &lt;code&gt;channels:read&lt;/code&gt;, and &lt;code&gt;chat:write&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(It will soon become clear why we need the channel permissions in addition to the more obvious &lt;code&gt;chat:write&lt;/code&gt; permission).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lastly, near the top of the page, click on &lt;strong&gt;Install to Workspace&lt;/strong&gt;. This will generate your new &lt;strong&gt;Bot User OAuth Token&lt;/strong&gt;. Again, copy the token and add it to your &lt;code&gt;.env&lt;/code&gt; file with the key &lt;code&gt;SLACK_BOT_USER_TOKEN&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;SLACK_BOT_USER_TOKEN=your-long-alpha-numeric-bot-token-here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can create a new file in the project root called &lt;code&gt;slack.py&lt;/code&gt; and add the code below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import os
from slack_sdk import WebClient
from dotenv import load_dotenv
load_dotenv()

def create_slack_message(items):
    SLACK_BOT_TOKEN = str(os.getenv(&amp;#39;SLACK_BOT_USER_TOKEN&amp;#39;))
    client = WebClient(token=SLACK_BOT_TOKEN)
    channel = &amp;quot;C0211DW58JD&amp;quot;

    if isinstance(items, str):
        text_to_send = items
    else:
        message_lines = [f&amp;quot;- {item[&amp;#39;Date&amp;#39;]}: {item[&amp;#39;Weather Description&amp;#39;]}&amp;quot; for item in items]
        text_to_send = &amp;quot;New rain alert(s):\n&amp;quot; + &amp;quot;\n&amp;quot;.join(message_lines)

    try:
        client.conversations_join(channel=channel)
    except Exception:
        pass

    client.chat_postMessage(channel=channel, text=text_to_send)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After instantiating the WebClient with our &lt;code&gt;SLACK_BOT_USER_TOKEN&lt;/code&gt;, we specify the Slack channel we are going to use for our notifications. This must be in ID form as the Slack SDK doesn&amp;#39;t understand plain language channel names. So first, in order to get your own unique channel ID, you need to temporarily run the &lt;code&gt;for&lt;/code&gt; loop below in your code (if you use the channel ID above, it will not work for your own unique installation):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# response = client.conversations_list()
# for channel in response[&amp;#39;channels&amp;#39;]:
#     print(channel[&amp;#39;name&amp;#39;], channel[&amp;#39;id&amp;#39;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, the &lt;code&gt;if isinstance(items, str)&lt;/code&gt; line checks if the &lt;code&gt;items&lt;/code&gt; variable is a plain language string to simply pass through the strings: &lt;code&gt;&amp;quot;No rain expected in the next 5 days&amp;quot;&lt;/code&gt; or &lt;code&gt;&amp;quot;No change in the forecast found over the next 5 days&amp;quot;&lt;/code&gt;. If it&amp;#39;s not a string, the code will process the list of dictionaries and display it as a nicely-formatted message for the Slack notification.&lt;/p&gt;
&lt;p&gt;The next &lt;code&gt;try/except&lt;/code&gt; block is just a convenience measure to ensure that the bot is a member of the channel. If it&amp;#39;s not, it makes sure that the bot can automatically join the channel. This saves us from having to manually add the bot as a channel member in the Slack UI (if the bot is not a channel member, we will get errors).&lt;/p&gt;
&lt;p&gt;Lastly, with just the line &lt;code&gt;client.chat_postMessage(channel=channel, text=text_to_send)&lt;/code&gt;, we will execute the &lt;code&gt;chat_postMessage&lt;/code&gt; and actually create the message notification.&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s go back to &lt;code&gt;github_data.py&lt;/code&gt;, import the &lt;code&gt;create_slack_message&lt;/code&gt; function, and call it in the three places inside &lt;code&gt;check_github_json&lt;/code&gt; and &lt;code&gt;update_github_json&lt;/code&gt;, as shown below (annotated with &lt;code&gt;#New&lt;/code&gt; for clarity):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import os
import logging
import json
from github import Github
#New
from apis.slack import create_slack_message
from datetime import datetime, timezone
from dotenv import load_dotenv
load_dotenv()
logger = logging.getLogger(__name__)

...

def check_github_json(forecast):

    if forecast is None:
        #New
        create_slack_message(&amp;quot;No rain expected in the next 5 days.&amp;quot;)
        return

    repo, file, db_data = load_github_json()
    new_items = []
    is_first_run = len(db_data) == 0

    for item in forecast:
        if item not in db_data:
            print(&amp;quot;Make JSON DB entry&amp;quot;)
            db_data.append(item)
            new_items.append(item)
        else:
            print(&amp;quot;Skip, found in DB&amp;quot;)

    if new_items:
        if is_first_run:
            update_github_json(repo, file, db_data, slack_items=new_items)
        else:
            update_github_json(repo, file, db_data, slack_items=[new_items[-1]])
    else:
        print(&amp;quot;No new items to commit&amp;quot;)
        #New
        create_slack_message(&amp;quot;No change in the forecast found over the next 5 days.&amp;quot;)


def update_github_json(repo, file, updated_data, slack_items):
    bytes_data = json.dumps(updated_data).encode(&amp;#39;utf-8&amp;#39;)
    commit_msg = datetime.now(timezone.utc).strftime(&amp;quot;Update weather data - %Y-%m-%d %H:%M:%S UTC&amp;quot;)
    try:
        repo.update_file(file.path, commit_msg, bytes_data, file.sha)
        print(&amp;quot;Github data updated!&amp;quot;)
        #New
        create_slack_message(slack_items)
    except Exception as e:
        logger.exception(&amp;quot;An error occurred&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, it might be a good idea to revert back to an empty array in our GitHub &lt;code&gt;weather_data&lt;/code&gt; repository, so we can run our next test from a clean starting point.&lt;/p&gt;
&lt;p&gt;Once that&amp;#39;s done, run &lt;code&gt;python main.py&lt;/code&gt; again.&lt;/p&gt;
&lt;p&gt;If everything is working smoothly, this should trigger a new Slack message notification:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-08/slack-rain-notification.png&quot; alt=&quot;Slack example showing a rain message notification&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Implement Celery for Python and RabbitMQ&lt;/h2&gt;
&lt;p&gt;Before we can start work on our Celery background task system, we need to make sure RabbitMQ is installed and running on our system.&lt;/p&gt;
&lt;p&gt;For Mac, the easiest way to install RabbitMQ is with Homebrew. To install on other operating systems, &lt;a href=&quot;https://www.rabbitmq.com/docs/download&quot;&gt;see the official RabbitMQ documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First, run these two installation commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;brew install erlang
brew install rabbitmq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To start RabbitMQ as a background service, run this command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;brew services start rabbitmq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also stop RabbitMQ at any time with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;brew services stop rabbitmq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we already installed Celery in our project with pip earlier, we can now move onto editing &lt;code&gt;main.py&lt;/code&gt; and adding the Celery functionality. Here is our new &lt;code&gt;main.py&lt;/code&gt;, with comments indicating where new code has been added:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import os
import requests
from celery import Celery
from celery.schedules import crontab
from github_data import check_github_json
from dotenv import load_dotenv
load_dotenv()

# New Celery Code:
app = Celery(&amp;#39;main&amp;#39;, broker=&amp;#39;amqp://localhost&amp;#39;)
app.conf.timezone = &amp;#39;Europe/London&amp;#39;
app.conf.broker_pool_limit = 1
app.conf.beat_schedule = {
    &amp;#39;check-weather-daily-at-noon&amp;#39;: {
        &amp;#39;task&amp;#39;: &amp;#39;main.weather_task&amp;#39;,
        &amp;#39;schedule&amp;#39;: crontab(hour=12, minute=0),
        &amp;#39;args&amp;#39;: (&amp;#39;London&amp;#39;,)
    },
}

# New decorator
@app.task(name=&amp;#39;main.weather_task&amp;#39;)
def weather_task(city):
    api_key = str(os.getenv(&amp;#39;OPEN_WEATHER_MAP_API_KEY&amp;#39;))
    url = &amp;quot;https://api.openweathermap.org/data/2.5/forecast&amp;quot;
    params = {&amp;quot;q&amp;quot;: city, &amp;quot;appid&amp;quot;: api_key, &amp;quot;units&amp;quot;: &amp;quot;metric&amp;quot;}
    response = requests.get(url, params=params)

    if response.status_code != 200:
        print(f&amp;quot;Error {response.status_code}: {response.text}&amp;quot;)
        return

    data = response.json()
    summaries = []

    for entry in data[&amp;quot;list&amp;quot;]:
        if &amp;quot;12:00:00&amp;quot; in entry[&amp;quot;dt_txt&amp;quot;]:
            main = entry[&amp;quot;weather&amp;quot;][0][&amp;quot;main&amp;quot;]
            description = entry[&amp;quot;weather&amp;quot;][0][&amp;quot;description&amp;quot;]

            if &amp;quot;rain&amp;quot; in description.lower() or &amp;quot;rain&amp;quot; in main.lower() or &amp;quot;rain&amp;quot; in entry:
                date = entry[&amp;quot;dt_txt&amp;quot;].split(&amp;quot; &amp;quot;)[0]
                summaries.append({
                    &amp;quot;Date&amp;quot;: date,
                    &amp;quot;Weather Description&amp;quot;: description
                })

    if summaries:
        check_github_json(summaries)
    else:
        check_github_json(None)

# Below used before Celery, commented-out now but kept if you want to test the API without Celery
# if __name__ == &amp;quot;__main__&amp;quot;:
# 	weather_task(&amp;quot;London&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main code that&amp;#39;s of interest in the above snippet is the block labelled &lt;strong&gt;&amp;quot;# New Celery Code&amp;quot;&lt;/strong&gt;. On the first line of this block, we create a new Celery application instance and connect it to the local RabbitMQ message broker using the AMQP protocol. Under this line, I&amp;#39;ve set the timezone to &lt;code&gt;&amp;#39;Europe/London&amp;#39;&lt;/code&gt; (since this is where I&amp;#39;m based), but you should change this to your own timezone.&lt;/p&gt;
&lt;p&gt;Next, we limit the number of RabbitMQ broker connections to 1 with &lt;code&gt;app.conf.broker_pool_limit = 1&lt;/code&gt;. (Note: you can increase this connection number if you encounter performance issues down the line.)&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;app.conf.beat_schedule&lt;/code&gt;, we create a configuration dictionary for our Celery scheduler. We call our overall &amp;quot;job&amp;quot;: &lt;code&gt;check-weather-daily-at-noon&lt;/code&gt;, specify that the task we want to run is the &lt;code&gt;main.weather_task&lt;/code&gt; function, and, using &lt;code&gt;crontab&lt;/code&gt;, set the task to run every day at 12pm. Finally, the &lt;code&gt;args&lt;/code&gt; key passes &amp;quot;London&amp;quot; as an argument to the function, so we receive updates on London weather.&lt;/p&gt;
&lt;p&gt;Now, instead of running &lt;code&gt;python main.py&lt;/code&gt;, we run our Celery beat and worker commands in one line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;celery -A main worker --loglevel=info --beat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything is working correctly, you should see something similar to the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;-------------- celery@Daniels-MacBook-Pro-4.local v5.5.2 (immunity)
--- ***** -----
-- ******* ---- macOS-10.16-x86_64-i386-64bit 2025-06-06 12:14:23
- *** --- * ---
- ** ---------- [config]
- ** ---------- .&amp;gt; app:         main:0x10c163a60
- ** ---------- .&amp;gt; transport:   amqp://guest:**@localhost:5672//
- ** ---------- .&amp;gt; results:     disabled://
- *** --- * --- .&amp;gt; concurrency: 8 (prefork)
-- ******* ---- .&amp;gt; task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
 -------------- [queues]
                .&amp;gt; celery           exchange=celery(direct) key=celery


[tasks]
  . main.weather_task
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to be 100% sure that the task will actually fire, you can play around with the hour and minute settings to make it close to your current time (which should finally trigger the message notification in Slack).&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s it!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;If you&amp;#39;ve made it this far, well done — we&amp;#39;ve done a lot of work together!&lt;/p&gt;
&lt;p&gt;In this tutorial, we brought together Celery, RabbitMQ, and three external APIs to create an automated rain notification service. We learned how to fetch weather forecasts with OpenWeatherMap, avoid duplicate alerts using GitHub as a lightweight JSON store, and deliver notifications via the Slack API. Finally, we used Celery and RabbitMQ to run the task automatically every day at noon.&lt;/p&gt;
&lt;p&gt;Whether you&amp;#39;re monitoring weather, syncing data, or triggering reminders, this project can serve as a foundation to help you build more background automation tasks in the near future.&lt;/p&gt;
&lt;p&gt;Happy automating!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to Use Redis with Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/08/20/how-to-use-redis-with-python.html"/>
    <id>https://blog.appsignal.com/2025/08/20/how-to-use-redis-with-python.html</id>
    <published>2025-08-20T00:00:00+00:00</published>
    <updated>2025-08-20T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll learn how to use Redis in Python with a step-by-step tutorial.</summary>
    <content type="html">&lt;p&gt;When it comes to data-driven applications, developers and data engineers are always trying to balance factors such as scalability, speed, flexibility, latency, and availability.&lt;/p&gt;
&lt;p&gt;In other words, databases and infrastructure are the foundations for well-structured applications: just like bricks are for houses.&lt;/p&gt;
&lt;p&gt;This article explores Redis&amp;#39; data store features and includes use cases. We&amp;#39;ll learn how to use Redis in Python with a step-by-step tutorial.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get started!&lt;/p&gt;
&lt;h2&gt;What Is Redis?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://redis.io/&quot;&gt;Redis&lt;/a&gt; (short for Remote Dictionary Server) is an open-source, in-memory data structure store that can be used as a database, cache, message broker, or queue. It is known for its high performance, low latency, and versatility as it stores data in memory (on the RAM) rather than on disk, which makes it extremely fast for read and write operations.&lt;/p&gt;
&lt;p&gt;Redis is often referred to as a data structure server because it provides access to mutable data structures via a set of commands, which are sent using a server-client model with TCP sockets and a simple protocol. This means that different processes can query and modify the same data structures in a shared way.&lt;/p&gt;
&lt;h2&gt;Why Use Redis?&lt;/h2&gt;
&lt;p&gt;Before describing Redis&amp;#39; most common use cases, let&amp;#39;s explore its main features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;In-memory storage&lt;/strong&gt;: In Redis, data is stored in RAM, providing response times in sub-milliseconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistence options&lt;/strong&gt;: While data is stored in memory, Redis also offers persistance options like RDB (Redis Database Backup) — periodic dataset snapshots — and AOF (Append-Only File), which logs every write operation for durability. This allows data to be saved to disk and replicated across multiple servers to ensure data durability and availability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pub/sub messaging&lt;/strong&gt;: It supports publish/subscribe messaging patterns for real-time communication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clustering and replication&lt;/strong&gt;: Redis supports clustering and primary-secondary replication, providing scalability and high availability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, at its core, Redis is a key-value store, meaning it stores data as a collection of keys and their associated values.&lt;/p&gt;
&lt;p&gt;A key in Redis is a unique string that acts as an identifier for the associated value. Keys are case-sensitive and can contain any binary sequence (e.g., strings, numbers, or special characters).&lt;/p&gt;
&lt;p&gt;A value in Redis can be one of several data types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Strings&lt;/li&gt;
&lt;li&gt;Hashes&lt;/li&gt;
&lt;li&gt;Lists&lt;/li&gt;
&lt;li&gt;Sets&lt;/li&gt;
&lt;li&gt;Sorted Sets&lt;/li&gt;
&lt;li&gt;Bitmaps&lt;/li&gt;
&lt;li&gt;HyperLogLogs&lt;/li&gt;
&lt;li&gt;Streams&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis provides a wide set of commands for working with key-value pairs, such as &lt;code&gt;SET&lt;/code&gt;, &lt;code&gt;GET&lt;/code&gt;, and &lt;code&gt;DEL&lt;/code&gt; for strings, &lt;code&gt;HSET&lt;/code&gt;, &lt;code&gt;HGET&lt;/code&gt;, and &lt;code&gt;HDEL&lt;/code&gt; for hashes, and &lt;code&gt;LPUSH&lt;/code&gt;, &lt;code&gt;LGET&lt;/code&gt;, and &lt;code&gt;LREM&lt;/code&gt; for lists. Here are some examples of key-value pairs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SET user:1001 &amp;quot;John Doe&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HSET user:1001 name &amp;quot;John Doe&amp;quot; age 30 email &amp;quot;john@example.com&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LPUSH tasks &amp;quot;task1&amp;quot; &amp;quot;task2&amp;quot; &amp;quot;task3&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The management of such data structures in Redis is important because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis stores them on disk, even if they are always served and modified into server memory. This means that Redis is fast, but also non-volatile.&lt;/li&gt;
&lt;li&gt;The implementation of data structures emphasizes memory efficiency, so data structures inside Redis will likely use less memory compared to the same data structure modelled using a high-level programming language.&lt;/li&gt;
&lt;li&gt;Redis offers a number of features commonly found in databases, such as replication, tunable durability levels, clustering, and high availability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this in mind, here are some common applications of Redis:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Caching&lt;/strong&gt;: As it stores data in memory, one of the main uses of Redis is as a caching system for web applications, since it reduces server load and improves page loading times.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-08/redis.png&quot; alt=&quot;How Redis caches web requests by Federico Trotta&quot;/&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Real-time applications&lt;/strong&gt;: Given its speed and reliability, Redis is perfect for real-time use cases like chat applications, real-time analytics, and live notifications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session management&lt;/strong&gt;: Another common use of Redis is storing user session data in web applications because of its speed and ability to handle expiration policies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Message queues&lt;/strong&gt;: Redis can act as a lightweight message broker using its pub/sub or streams features, making it suitable for event-driven architectures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced data structures management&lt;/strong&gt;: Redis supports advanced data structures that are not available in traditional databases, making it useful for tasks like counting unique visitors (HyperLogLog), storing time-series data (Streams), and implementing priority queues (Sorted Sets).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to Use Redis in Python: A Step-By-Step Tutorial&lt;/h2&gt;
&lt;p&gt;Now that we&amp;#39;ve covered the theory, it&amp;#39;s time to see how to use Redis in Python.&lt;/p&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;To replicate this tutorial, your system must have the following prerequisites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.6 or higher&lt;/a&gt;&lt;/strong&gt;: Any Python version higher than 3.6 will do. Specifically, we will install dependencies via pip (already included in any Python version from 3.4+).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 1: Setting Up the Environment and Installing Dependencies&lt;/h3&gt;
&lt;p&gt;Suppose you call the main folder of your project &lt;code&gt;redis&lt;/code&gt;. The project structure will be as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;redis/
│
├── venv/
└── examples/
   ├── basic_connection.py
   ├── strings_example.py
   ├── hashes_example.py
   ├── sets_example.py
   ├── transactions_example.py
   └── expiration.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;.py&lt;/code&gt; files will contain the logic described in the next steps.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;venv/&lt;/code&gt; contains the virtual environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can create the &lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;&lt;code&gt;venv/&lt;/code&gt; virtual environment&lt;/a&gt; directory like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To activate it on Windows, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On macOS/Linux, execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the activated virtual environment, install Redis:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install redis
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2: Testing The Connection to Redis&lt;/h2&gt;
&lt;p&gt;To establish a connection to a Redis server using &lt;code&gt;redis-py&lt;/code&gt;, type the following code into the &lt;code&gt;basic_connection.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)

print(r.ping())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run it via &lt;code&gt;python3 basic_connection.py&lt;/code&gt;, if everything works fine, you will receive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;True
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Managing Data Structures&lt;/h3&gt;
&lt;p&gt;We&amp;#39;ve learned that Redis stores data as key-value pairs, and the values can be different data structures. Now it&amp;#39;s time to see how this works.&lt;/p&gt;
&lt;p&gt;To manage strings, type the following in &lt;code&gt;strings_example.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)

r.set(&amp;#39;user:1001&amp;#39;, &amp;#39;John Doe&amp;#39;)
print(r.get(&amp;#39;user:1001&amp;#39;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which, as expected, returns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;John Doe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To manage hashes, type the following in the &lt;code&gt;hashes_example.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)

r.hset(&amp;#39;user:1&amp;#39;, &amp;#39;name&amp;#39;, &amp;#39;Alice&amp;#39;)
print(r.hget(&amp;#39;user:1&amp;#39;, &amp;#39;name&amp;#39;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which returns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Alice
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To manage sets, write the following into the &lt;code&gt;sets_example.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)

r.sadd(&amp;#39;tags&amp;#39;, &amp;#39;python&amp;#39;, &amp;#39;redis&amp;#39;)
print(r.smembers(&amp;#39;tags&amp;#39;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which returns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;{&amp;#39;redis&amp;#39;, &amp;#39;python&amp;#39;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Using Advanced Redis Features&lt;/h3&gt;
&lt;p&gt;Redis provides various advanced features, including pipelines.&lt;/p&gt;
&lt;p&gt;A &lt;a href=&quot;https://redis.io/docs/latest/develop/use/pipelining/&quot;&gt;pipeline in Redis&lt;/a&gt; is a way to batch multiple commands together and send them to the Redis server in a single network request. This reduces the overhead of multiple round trips between the client and the server, improving performance, especially when executing multiple commands.&lt;/p&gt;
&lt;p&gt;For example, if you want to create a pipeline object (pipe) that queues commands instead of executing them immediately, type the following into the &lt;code&gt;transactions_example.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)

with r.pipeline() as pipe:
    pipe.set(&amp;#39;key1&amp;#39;, &amp;#39;value1&amp;#39;)
    pipe.set(&amp;#39;key2&amp;#39;, &amp;#39;value2&amp;#39;)
    pipe.execute()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;pipe.execute()&lt;/code&gt; method sends all the queued commands in the pipeline to the Redis server in a single request. Then, the server processes the commands in the order they are queued.&lt;/p&gt;
&lt;p&gt;Another advanced feature of Redis is the possibility to set keys with expiration times, retrieve them before and after expiration, and handle scenarios where the keys no longer exist. To do so, write the following into the &lt;code&gt;expiration.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
import redis

# Connect to the Redis server
r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)

# Set keys with expiration using setex()
print(&amp;quot;Setting keys with expiration...&amp;quot;)
r.setex(&amp;#39;temp_key1&amp;#39;, 5, &amp;#39;value1&amp;#39;)  # Expires in 5 seconds
r.setex(&amp;#39;temp_key2&amp;#39;, 10, &amp;#39;value2&amp;#39;)  # Expires in 10 seconds

# Retrieve the keys before expiration
print(&amp;quot;\nRetrieving keys before expiration:&amp;quot;)
print(f&amp;quot;temp_key1: {r.get(&amp;#39;temp_key1&amp;#39;)}&amp;quot;)
print(f&amp;quot;temp_key2: {r.get(&amp;#39;temp_key2&amp;#39;)}&amp;quot;)

# Wait for 6 seconds
print(&amp;quot;\nWaiting for 6 seconds...&amp;quot;)
time.sleep(6)

# Try retrieving the keys after expiration
print(&amp;quot;\nRetrieving keys after 6 seconds:&amp;quot;)
print(f&amp;quot;temp_key1: {r.get(&amp;#39;temp_key1&amp;#39;)}&amp;quot;)
print(f&amp;quot;temp_key2: {r.get(&amp;#39;temp_key2&amp;#39;)}&amp;quot;)

# Wait for another 5 seconds
print(&amp;quot;\nWaiting for another 5 seconds...&amp;quot;)
time.sleep(5)

# Try retrieving the keys again
print(&amp;quot;\nRetrieving keys after 11 seconds:&amp;quot;)
print(f&amp;quot;temp_key1: {r.get(&amp;#39;temp_key1&amp;#39;)}&amp;quot;)
print(f&amp;quot;temp_key2: {r.get(&amp;#39;temp_key2&amp;#39;)}&amp;quot;)

# Set a key with expiration and check its TTL
print(&amp;quot;\nSetting a new key with expiration...&amp;quot;)
r.setex(&amp;#39;temp_key3&amp;#39;, 15, &amp;#39;value3&amp;#39;)
print(f&amp;quot;temp_key3: {r.get(&amp;#39;temp_key3&amp;#39;)}&amp;quot;)
print(f&amp;quot;TTL for temp_key3: {r.ttl(&amp;#39;temp_key3&amp;#39;)} seconds&amp;quot;)

# Wait for 5 seconds and check TTL again
print(&amp;quot;\nWaiting for 5 seconds...&amp;quot;)
time.sleep(5)
print(f&amp;quot;TTL for temp_key3 after 5 seconds: {r.ttl(&amp;#39;temp_key3&amp;#39;)} seconds&amp;quot;)

# Delete the key before it expires
print(&amp;quot;\nDeleting temp_key3 before it expires...&amp;quot;)
r.delete(&amp;#39;temp_key3&amp;#39;)
print(f&amp;quot;temp_key3: {r.get(&amp;#39;temp_key3&amp;#39;)}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is what this code does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;setex(name, time, value)&lt;/code&gt; method sets a key with a specific expiration time (in seconds). In the provided example, the keys have different expiration times.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;get()&lt;/code&gt; method retrieves the value of a key. Before the expiration time, the keys return their respective values.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ttl()&lt;/code&gt; method retrieves the remaining time (in seconds) before a key expires.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is the expected result after the whole process is completed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Setting keys with expiration...

Retrieving keys before expiration:
temp_key1: b&amp;#39;value1&amp;#39;
temp_key2: b&amp;#39;value2&amp;#39;

Waiting for 6 seconds...

Retrieving keys after 6 seconds:
temp_key1: None
temp_key2: b&amp;#39;value2&amp;#39;

Waiting for another 5 seconds...

Retrieving keys after 11 seconds:
temp_key1: None
temp_key2: None

Setting a new key with expiration...
temp_key3: b&amp;#39;value3&amp;#39;
TTL for temp_key3: 15 seconds

Waiting for 5 seconds...
TTL for temp_key3 after 5 seconds: 10 seconds

Deleting temp_key3 before it expires...
temp_key3: None
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How to Use Redis in Python for Monitoring&lt;/h2&gt;
&lt;p&gt;Monitoring a Redis server is an essential aspect of managing a production system, for more than just checking general statistics.&lt;/p&gt;
&lt;p&gt;Redis offers built-in methods for monitoring, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;info()&lt;/code&gt;: Providing comprehensive details about memory usage, client connections, CPU utilization, and other important statistics.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;monitor()&lt;/code&gt;: Streaming every command processed by the Redis server in real time. (However, consider that using this in a production environment can be resource-intensive and may impact performance.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client_list()&lt;/code&gt;: Returning details about connected clients, which is useful for identifying unusual client behavior or ensuring your connection pools are behaving as expected.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;slowlog_get()&lt;/code&gt;: Allowing you to track slow-executing commands to pinpoint potential bottlenecks or inefficient queries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;#39;s how to implement the above monitoring methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
import threading
import time

def server_stats_monitor():
    &amp;quot;&amp;quot;&amp;quot;
    Periodically retrieves and prints key Redis server statistics.
    It uses INFO to extract memory usage, client counts, CPU usage, and uptime.
    &amp;quot;&amp;quot;&amp;quot;
    r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)

    while True:
        info = r.info()
        print(&amp;quot;=== Redis Server Monitoring ===&amp;quot;)
        print(&amp;quot;Used Memory:&amp;quot;, info.get(&amp;quot;used_memory_human&amp;quot;))
        print(&amp;quot;Connected Clients:&amp;quot;, info.get(&amp;quot;connected_clients&amp;quot;))
        print(&amp;quot;CPU Usage:&amp;quot;, info.get(&amp;quot;used_cpu_sys&amp;quot;), &amp;quot;system CPU seconds,&amp;quot;,
              info.get(&amp;quot;used_cpu_user&amp;quot;), &amp;quot;user CPU seconds&amp;quot;)
        print(&amp;quot;Uptime (seconds):&amp;quot;, info.get(&amp;quot;uptime_in_seconds&amp;quot;))
        print()  # Blank line for readability

        # Retrieve and display slow log entries (if any)
        slow_logs = r.slowlog_get()
        print(&amp;quot;=== Slow Log Entries ===&amp;quot;)
        if slow_logs:
            for log in slow_logs:
                # Each log entry is a dictionary with keys like &amp;#39;id&amp;#39;, &amp;#39;start_time&amp;#39;, &amp;#39;duration&amp;#39;, and &amp;#39;command&amp;#39;
                print(f&amp;quot;ID: {log.get(&amp;#39;id&amp;#39;)}, Duration: {log.get(&amp;#39;duration&amp;#39;)} μs, Command: {log.get(&amp;#39;command&amp;#39;)}&amp;quot;)
        else:
            print(&amp;quot;No slow log entries found.&amp;quot;)
        print()

        # Retrieve and display client list details
        client_list = r.client_list()
        print(&amp;quot;=== Connected Clients ===&amp;quot;)
        for client in client_list:
            # Displaying each client info (address, age, idle time, flags, db, etc.)
            print(f&amp;quot;Address: {client.get(&amp;#39;addr&amp;#39;)} | Idle Time: {client.get(&amp;#39;idle&amp;#39;)} sec | DB: {client.get(&amp;#39;db&amp;#39;)}&amp;quot;)
        print(&amp;quot;\n-----------------------------\n&amp;quot;)

        time.sleep(10)  # Pause for 10 seconds before repeating

# Start the monitoring in a background thread so that it runs continuously.
stats_thread = threading.Thread(target=server_stats_monitor)
stats_thread.daemon = True
stats_thread.start()


def monitor_commands():
    &amp;quot;&amp;quot;&amp;quot;
    Sets up a real-time monitor that listens to every command processed by Redis.
    Note: Running MONITOR can be heavy on performance and should be used cautiously.
    &amp;quot;&amp;quot;&amp;quot;
    r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)
    monitor = r.monitor()
    for command in monitor.listen():
        print(&amp;quot;Real-time Command:&amp;quot;, command)


# Keep the main thread active so the background monitoring continues.
while True:
    time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It prints server statistics every ten seconds using the &lt;code&gt;info()&lt;/code&gt; method.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;slowlog_get()&lt;/code&gt; gets slow log entries, if any. Each entry is typically a dictionary containing an identifier, start time, duration (in microseconds), and the command that ran slowly. This helps you to identify any commands that might be causing performance issues.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;client_list()&lt;/code&gt; gets the connected clients&amp;#39; status. The returned result is a list of dictionaries, each containing details such as the client&amp;#39;s address, idle time, connected database, and other flags. Monitoring these details can help you find issues with idle or misbehaving clients.&lt;/li&gt;
&lt;li&gt;An optional real-time monitor using &lt;code&gt;monitor()&lt;/code&gt; is provided to get real time data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The expected result is something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;=== Redis Server Monitoring ===
Used Memory: 852.59Kz\
Connected Clients: 1
CPU Usage: 2.931738 system CPU seconds, 1.491924 user CPU seconds
Uptime (seconds): 1773

=== Slow Log Entries ===
No slow log entries found.

=== Connected Clients ===
Address: xxx.x.x.x:xxxxx | Idle Time: 0 sec | DB: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, of course, it will continue to provide statistics in a loop:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-08/redis-monitoring.png&quot; alt=&quot;Monitoring Redis&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Monitoring Redis with AppSignal&lt;/h3&gt;
&lt;p&gt;While Redis provides built-in monitoring features, they are low-level, infrastructure-focused metrics for understanding raw performance, resource consumption, and debugging specific issues within the Redis server itself.&lt;/p&gt;
&lt;p&gt;If you want to develop a more detailed understanding of your application&amp;#39;s overall health, you can use &lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/redis.html&quot;&gt;AppSignal&amp;#39;s integration with Redis&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;AppSignal provides performance metrics, error tracking, and logging for your applications.&lt;/p&gt;
&lt;p&gt;To use AppSignal, first &lt;a href=&quot;https://appsignal.com/users/sign_up&quot;&gt;sign up for a 30-day free trial&lt;/a&gt;, use the &lt;a href=&quot;https://docs.appsignal.com/python/installation.html&quot;&gt;Python installation guide&lt;/a&gt; and &lt;a href=&quot;https://docs.appsignal.com/python/configuration&quot;&gt;configuration guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then you can integrate AppSignal with Redis like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
import threading
import time
from appsignal import Appsignal, probes, set_gauge

# Initialize the AppSignal client with your AppSignal push API key and application name.
appsignal = Appsignal(
    push_api_key=&amp;quot;YOUR_APPSIGNAL_PUSH_API_KEY&amp;quot;,
    name=&amp;quot;redis_monitor&amp;quot;,
    active=True
)

def monitor_redis_with_appsignal():
    &amp;quot;&amp;quot;&amp;quot;
    This function collects Redis monitoring data and sends selected metrics
    to AppSignal using its Python API. Metrics include memory usage, client counts,
    uptime, slow log details, and more.
    &amp;quot;&amp;quot;&amp;quot;
    # Connect to your Redis server
    r = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, db=0)

    # Collect basic server stats using the INFO command
    info = r.info()
    metrics = {
        &amp;quot;redis.used_memory_bytes&amp;quot;: info.get(&amp;quot;used_memory&amp;quot;),
        &amp;quot;redis.used_memory_human&amp;quot;: info.get(&amp;quot;used_memory_human&amp;quot;),
        &amp;quot;redis.connected_clients&amp;quot;: info.get(&amp;quot;connected_clients&amp;quot;),
        &amp;quot;redis.cpu_sys&amp;quot;: info.get(&amp;quot;used_cpu_sys&amp;quot;),
        &amp;quot;redis.cpu_user&amp;quot;: info.get(&amp;quot;used_cpu_user&amp;quot;),
        &amp;quot;redis.uptime_seconds&amp;quot;: info.get(&amp;quot;uptime_in_seconds&amp;quot;)
    }

    # Use the AppSignal API to record each metric.
    for metric_name, value in metrics.items():
        set_gauge(metric_name, value)

probes.register(&amp;quot;redis&amp;quot;, monitor_redis_with_appsignal)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example instantiates the &lt;code&gt;Appsignal&lt;/code&gt; class to send Redis monitoring metrics directly to AppSignal. This approach allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Correlate Redis data with transaction traces and error reports from the rest of your application. For example, if a spike in &lt;code&gt;redis.slowlog_count&lt;/code&gt; coincides with a surge in request latency or error notifications, you have immediate insight into a potential bottleneck or failure point.&lt;/li&gt;
&lt;li&gt;Visualize metric history over time, as AppSignal stores historical data. This allows you to identify trends, seasonal variations, or gradual degradations in Redis performance — insights that are not as obvious when only using Redis’s native monitoring features.&lt;/li&gt;
&lt;li&gt;Set up &lt;a href=&quot;https://docs.appsignal.com/application/notification-settings.html&quot;&gt;custom alerts in AppSignal&lt;/a&gt; that are triggered when certain thresholds are breached.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that&amp;#39;s it for this post!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, you learned what Redis is, why you should use it, and how to use it with Python.&lt;/p&gt;
&lt;p&gt;We also explored how to use Redis&amp;#39; monitoring features and set up AppSignal to monitor Redis for more in-depth insights into your application.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Deploy a Python Flask App to Render with Docker</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/08/06/deploy-a-python-flask-app-to-render-with-docker.html"/>
    <id>https://blog.appsignal.com/2025/08/06/deploy-a-python-flask-app-to-render-with-docker.html</id>
    <published>2025-08-06T00:00:00+00:00</published>
    <updated>2025-08-06T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s build and optimize a Flask app for deployment to Render using Docker.</summary>
    <content type="html">&lt;p&gt;In this tutorial, we will build a Flask app with Docker, create a background worker with Celery, and use RabbitMQ as the message broker between the two services. This (relatively) simple example will be used to demonstrate how Docker makes it easier to run multiple services and share configuration details between developers.&lt;/p&gt;
&lt;p&gt;In the last section, we will also build and optimize our app for deployment to Render, so we can avoid some of the common gotchas it is easy to fall victim to along the way!&lt;/p&gt;
&lt;p&gt;First, let&amp;#39;s quickly touch on why we&amp;#39;re using Flask, Docker, and Render.&lt;/p&gt;
&lt;h2&gt;Why Flask?&lt;/h2&gt;
&lt;p&gt;Flask is a lightweight and flexible Python web framework that&amp;#39;s ideal for small to medium-sized applications. Flask provides both a high degree of simplicity and extensibility via Python&amp;#39;s rich package library. This makes it a great choice for rapid prototyping and microservices.&lt;/p&gt;
&lt;p&gt;On the downside, Flask lacks built-in features found in more comprehensive frameworks like Django. This means you will need to integrate common app functionality like user authorization/authentication, form handling, and database management from scratch.&lt;/p&gt;
&lt;h2&gt;Why Docker?&lt;/h2&gt;
&lt;p&gt;Docker allows developers to wrap applications with dependencies into a single virtual container, ensuring consistency across development, testing, and production environments. This makes it easier to manage complex setups, simplifies deployment, and avoids the typical developer refrain: &amp;quot;but it works on my machine&amp;quot;. Docker also integrates well with CI/CD pipelines and supports orchestration tools like Docker Compose and Kubernetes.&lt;/p&gt;
&lt;p&gt;But Docker does introduce an additional layer of complexity and a steeper learning curve. Resource usage can also be higher compared to running an app directly on your local machine without a virtual container.&lt;/p&gt;
&lt;h2&gt;Why Render?&lt;/h2&gt;
&lt;p&gt;Render offers a developer-friendly platform for deploying web apps, APIs, static sites, background workers, and more. It automates deployment from Git repositories, provides free SSL, custom domains, and offers managed services like PostgreSQL and Redis. Render’s simplicity and pay-as-you-go pricing model are particularly suited for startups and small developer teams.&lt;/p&gt;
&lt;p&gt;That said, Render has limitations in the level of customization offered compared to more mature platforms like AWS or GCP. For teams with specific infrastructure needs or strict performance requirements, you might need to consider something else.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s turn to installing Docker on our machine next.&lt;/p&gt;
&lt;h2&gt;Docker Installation&lt;/h2&gt;
&lt;p&gt;In this section, we will not follow the recommended approach of installing Docker Desktop for macOS. At the time of writing, my laptop running macOS Ventura with Apple&amp;#39;s M-series silicon chip flags a &lt;a href=&quot;https://www.bleepingcomputer.com/news/security/docker-desktop-blocked-on-macs-due-to-false-malware-alert/&quot;&gt;false security certificate issue&lt;/a&gt;. Windows users can also &lt;a href=&quot;https://docs.docker.com/desktop/setup/install/windows-install/&quot;&gt;check out the official documentation&lt;/a&gt;, but please note this hasn&amp;#39;t been battle-tested like the following macOS installation steps.&lt;/p&gt;
&lt;p&gt;The Docker Desktop security issue is not just alarming in itself, but also a major headache! Of course, there is a fix: manually adding the certificate via the command line and removing the (rather dramatic) macOS warning. But even after doing this, the Mac operating system appears to silently block the Docker Desktop app from launching. This is very frustrating, to say the least!&lt;/p&gt;
&lt;p&gt;So, to save you from all the drama, I recommend installing the necessary Docker app components manually from the command line (Docker Desktop automatically bundles all these individual components together). Let&amp;#39;s go through the steps one by one.&lt;/p&gt;
&lt;p&gt;First, use Homebrew to install Docker&amp;#39;s main command line interface (CLI) application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;brew install docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, install Colima, a lightweight Docker virtual machine (VM) which works well on Macs with M1 and M2 chips:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;brew install colima
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;colima start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a result, you should see this output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;INFO[0001] starting colima
INFO[0001] runtime: docker
INFO[0002] starting ...                                  context=vm
INFO[0015] provisioning ...                              context=docker
INFO[0016] starting ...                                  context=docker
INFO[0018] done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can stop Colima at any point with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;colima stop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the &lt;code&gt;docker compose up&lt;/code&gt; command in the next section when we build our sample Flask app, we need to install &lt;code&gt;docker-compose&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;brew install docker-compose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, if you have previously attempted to install Docker Desktop (like me!), you will need to separately install the Docker credentials helper component. You will also have to remove any old references to how Docker Desktop handles credentials in one of the configuration files.&lt;/p&gt;
&lt;p&gt;So now run this command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;brew install docker-credential-helper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open the hidden configuration file: &lt;code&gt;~/.docker/config.json&lt;/code&gt; and make sure the following line in the JSON file looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;credsStore&amp;quot;: &amp;quot;osxkeychain&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can test that everything is working correctly by running Docker&amp;#39;s built-in &amp;quot;hello world&amp;quot; command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hopefully, you will get the output below, and we can start building!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the &amp;quot;hello-world&amp;quot; image from the Docker Hub.
    (arm64v8)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create Your Python Flask App With Docker&lt;/h2&gt;
&lt;p&gt;First, let&amp;#39;s create a new project directory called &lt;code&gt;flask-docker-render&lt;/code&gt; and an &lt;code&gt;app.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;mkdir flask-docker-render
cd flask-docker-render
touch app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; At any time, you can &lt;a href=&quot;https://github.com/daneasterman/flask-docker-appsignal&quot;&gt;view or make your own copy of the full code&lt;/a&gt; used in this article.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, let&amp;#39;s go ahead and install Flask and Celery in our project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pip install Flask gunicorn celery python-dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;To update your &lt;code&gt;requirements.txt&lt;/code&gt; file with the newly-installed packages, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pip freeze &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see Flask and Celery appear in your requirements file (along with some other associated packages):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;amqp==5.3.1
async-timeout==5.0.1
billiard==4.2.1
blinker==1.9.0
celery==5.5.1
click==8.1.8
click-didyoumean==0.3.1
click-plugins==1.1.1
click-repl==0.3.0
Flask==3.1.0
gunicorn==23.0.0
itsdangerous==2.2.0
Jinja2==3.1.6
kombu==5.5.2
MarkupSafe==3.0.2
packaging==24.2
prompt_toolkit==3.0.50
python-dateutil==2.9.0.post0
python-dotenv==1.1.0
six==1.17.0
tzdata==2025.2
vine==5.1.0
wcwidth==0.2.13
Werkzeug==3.1.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, create &lt;code&gt;config.py&lt;/code&gt; in your root and add:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;class Config:
    DEBUG = False
    DEVELOPMENT = False
    CSRF_ENABLED = True

class ProductionConfig(Config):
    pass

class DevelopmentConfig(Config):
  DEBUG = True
  DEVELOPMENT = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now add this code to your &lt;code&gt;app.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# app.py

import os
from flask import Flask, jsonify
from tasks import generate_report
from dotenv import load_dotenv
load_dotenv()

app = Flask(__name__)

env_config = os.getenv(&amp;quot;PROD_APP_SETTINGS&amp;quot;, &amp;quot;config.DevelopmentConfig&amp;quot;)
app.config.from_object(env_config)

@app.route(&amp;#39;/start-task/&amp;#39;)
def start_task():
    print(&amp;quot;📬 /start-task was called!&amp;quot;)
    task = generate_report.delay()
    return jsonify({&amp;quot;task_id&amp;quot;: task.id, &amp;quot;status&amp;quot;: &amp;quot;started&amp;quot;}), 202

if __name__ == &amp;#39;__main__&amp;#39;:
    app.run(host=&amp;#39;0.0.0.0&amp;#39;, port=5000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;app.py&lt;/code&gt;, we build a very minimal Flask app that creates a &lt;code&gt;/start-task/&lt;/code&gt; route and imports the &lt;code&gt;generate_report&lt;/code&gt; function from &lt;code&gt;tasks.py&lt;/code&gt;. Let&amp;#39;s add that below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# tasks.py

import time
import os
from celery import Celery
from dotenv import load_dotenv
load_dotenv()

broker_url = os.environ.get(&amp;quot;CELERY_BROKER_URL&amp;quot;, &amp;quot;amqp://guest:guest@rabbitmq:5672//&amp;quot;)
celery_app = Celery(&amp;#39;tasks&amp;#39;, broker=broker_url)

@celery_app.task
def generate_report():
    time.sleep(5)
    return &amp;quot;Report complete!&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;tasks.py&lt;/code&gt;, we initialize a Celery app and simulate a slow background task by using Python&amp;#39;s built-in time module to add a 10-second delay. By separating the core Flask app from the Celery worker code, we ensure that our project is more modular and maintainable.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; We have also supplied the environment variable &lt;code&gt;CELERY_BROKER_URL&lt;/code&gt;, which will come in handy in the next deployment section. (For now, in local development mode, the code just grabs the default local rabbitmq broker url).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next, we will create a &lt;code&gt;Dockerfile&lt;/code&gt; in the project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM python:3.10-alpine

WORKDIR /app

RUN apk add --no-cache gcc musl-dev linux-headers

COPY requirements.txt requirements.txt

RUN pip install -r requirements.txt

COPY . .

EXPOSE 5000

RUN find . -name &amp;quot;*.pyc&amp;quot; -delete

CMD [&amp;quot;gunicorn&amp;quot;, &amp;quot;-b&amp;quot;, &amp;quot;0.0.0.0:5000&amp;quot;, &amp;quot;app:app&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; provides all the instructions needed to run a virtual container on our local machine. Let&amp;#39;s break down each instruction here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;FROM python:3.10-alpine&lt;/code&gt;: We specify the Python version number and tell Docker we want the lightweight Alpine Linux version for our Linux container.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORKDIR /app&lt;/code&gt;: Set the working directory to &lt;code&gt;/app&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RUN apk add --no-cache gcc musl-dev linux-headers&lt;/code&gt;: Using the &lt;code&gt;apk&lt;/code&gt; Linux Alpine package manager, we install the system-level build tools needed to compile our Python packages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COPY requirements.txt requirements.txt&lt;/code&gt;: Copy the &lt;code&gt;requirements.txt&lt;/code&gt; from the source local project to the container.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RUN pip install -r requirements.txt&lt;/code&gt;: Install all the packages listed in &lt;code&gt;requirements.txt&lt;/code&gt; inside the container.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COPY ..&lt;/code&gt;: copy everything in your local project to the container (excluding anything in &lt;code&gt;.dockerignore&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EXPOSE 5000&lt;/code&gt;: Tell Docker to listen on port 5000.&lt;/li&gt;
&lt;li&gt;Delete stale &lt;code&gt;.pyc&lt;/code&gt; files that are not needed and can cause problems for later deployments.&lt;/li&gt;
&lt;li&gt;Tell Docker to run the Flask app using the production-grade Gunicorn web server and use &lt;code&gt;app.py&lt;/code&gt; as the application entry point.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We also need to create a special yaml file for Docker called &lt;code&gt;compose.yaml&lt;/code&gt; in the project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  web:
    build: .
    ports:
      - &amp;quot;8000:5000&amp;quot;
    depends_on:
      - rabbitmq

  worker:
    build: .
    depends_on:
      - rabbitmq
    command: celery -A tasks.celery_app worker --loglevel=info

  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - &amp;quot;5672:5672&amp;quot;
      - &amp;quot;15672:15672&amp;quot; # Management UI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;compose.yaml&lt;/code&gt;, we have a single configuration file to manage several services required for our project. In this case, the Flask app triggers the tasks (mapping the default Flask 5000 port to a custom 8000 port), a Celery worker that works in the background to complete the tasks, and the RabbitMQ server, which acts as the &amp;quot;broker&amp;quot; between both services. The &lt;code&gt;rabbitmq&lt;/code&gt; section downloads the official RabbitMQ image from Docker Hub (once) and stores it for future use.&lt;/p&gt;
&lt;p&gt;Lastly, we also need a separate &lt;code&gt;Dockerfile.worker&lt;/code&gt; for our main Celery command to make sure that Celery runs correctly when we deploy to Render later in this tutorial (this file doesn&amp;#39;t have any impact on our local Docker setup). We need this additional file as, unfortunately, Render does not currently support &lt;code&gt;compose.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In the next deployment section, we will also need Render&amp;#39;s own &lt;code&gt;render.yaml&lt;/code&gt; file to ensure everything works smoothly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# Dockerfile.worker

FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD [&amp;quot;celery&amp;quot;, &amp;quot;-A&amp;quot;, &amp;quot;tasks.celery_app&amp;quot;, &amp;quot;worker&amp;quot;, &amp;quot;--loglevel=info&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Dockerfile.worker&lt;/code&gt; is very similar to the &lt;code&gt;Dockerfile&lt;/code&gt; for our Flask web service. The key difference here is the &lt;code&gt;CMD&lt;/code&gt; (startup command). This tells Celery to start a worker service and look in &lt;code&gt;tasks.py&lt;/code&gt; for the &lt;code&gt;celery_app&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;With those &lt;strong&gt;five&lt;/strong&gt; key files ready (&lt;code&gt;app.py&lt;/code&gt;, &lt;code&gt;tasks.py&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;Dockerfile.worker&lt;/code&gt;, and &lt;code&gt;compose.yaml&lt;/code&gt;) we can now build and start Flask, Celery, and RabbitMQ all with one Docker command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;docker compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first time you run this command, you will see output that shows that RabbitMQ has been downloaded from the official Docker Hub image to the Docker container on your local machine. Any subsequent times you run the command, you will just see the boot-up output for Flask, Celery, and RabbitMQ.&lt;/p&gt;
&lt;p&gt;Go to &lt;code&gt;localhost:8000/start-task&lt;/code&gt; in your browser. If everything works correctly, you should see something very similar to the JSON below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;status&amp;quot;: &amp;quot;started&amp;quot;,
  &amp;quot;task_id&amp;quot;: &amp;quot;14361d15-3a8e-4f73-8ed0-7f7fefc36efd&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: Your &lt;code&gt;task_id&lt;/code&gt; will differ, as it is randomly generated each time.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To verify that the delayed Celery task is working, when you hit the &lt;code&gt;start-task&lt;/code&gt; endpoint, you should see the following in your terminal logs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;[2025-04-15 16:00:16,306: INFO/MainProcess] Task tasks.generate_report[b7f6f260-5087-4893-b6ea-110888f620a6] received
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, 10 seconds later, the task will show as successfully completed in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;[2025-04-15 16:00:26,331: INFO/ForkPoolWorker-2] Task tasks.generate_report[b7f6f260-5087-4893-b6ea-110888f620a6] succeeded in 10.017975573999138s: &amp;#39;Report complete!&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Well done for making it this far! Now that we have everything working correctly on our local machine, it&amp;#39;s time to deploy our project to Render!&lt;/p&gt;
&lt;h2&gt;Deploy Your Python Flask Project to Render&lt;/h2&gt;
&lt;p&gt;When deploying to Render, first create a &lt;code&gt;render.yaml&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  - type: web
    name: flask-web
    env: docker
    plan: starter
    dockerfilePath: ./Dockerfile
    dockerContext: .
    envVars:
      - key: PROD_APP_SETTINGS
        value: config.ProductionConfig
      - key: CELERY_BROKER_URL
        sync: false

  - type: worker
    name: celery-worker
    env: docker
    plan: starter
    dockerfilePath: ./Dockerfile.worker
    dockerContext: .
    envVars:
      - key: CELERY_BROKER_URL
        sync: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;render.yaml&lt;/code&gt; file acts as a blueprint to define all the infrastructure needed to deploy our app (in fact, Render actually calls this &amp;quot;Blueprints&amp;quot; in its UI). This means that if you have multiple services to deploy, everything is explicitly defined in the code. You don&amp;#39;t have to add and configure each service in the UI. Also, when you set up everything via Render&amp;#39;s Blueprint approach, you just have to push your &lt;code&gt;main&lt;/code&gt; branch to GitHub, and it will trigger an automatic deploy for all your services.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a quick explanation of everything that&amp;#39;s happening in the &lt;code&gt;render.yaml&lt;/code&gt; above. We define two services: one is a web service (our Flask app) and the second is the background Celery worker. Both are using Docker as their environment and are on the Render starter plan ($7 per month). One important thing to note is the two different Docker files used: the web service uses the plain &lt;code&gt;Dockerfile&lt;/code&gt;, while the Celery worker uses &lt;code&gt;Dockerfile.worker&lt;/code&gt; (both in the project root).&lt;/p&gt;
&lt;p&gt;Lastly, the environment variables are pre-populated with a key and value for the web service. Since &lt;code&gt;config.ProductionConfig&lt;/code&gt; is not sensitive information (but just used to automatically rotate between local and production environments), we have included this in the code. But the &lt;code&gt;CELERY_BROKER_URL&lt;/code&gt; production environment variable should &lt;em&gt;&lt;strong&gt;never&lt;/strong&gt;&lt;/em&gt; be hardcoded. So we just provide the key for this, tell Render &lt;strong&gt;&lt;em&gt;not&lt;/em&gt;&lt;/strong&gt; to sync, and later we will manually enter the value in the Render UI.&lt;/p&gt;
&lt;h3&gt;Create CloudAMQP Instance&lt;/h3&gt;
&lt;p&gt;Now that we have our &lt;code&gt;render.yaml&lt;/code&gt; file ready, let&amp;#39;s sign up for an account at &lt;a href=&quot;https://customer.cloudamqp.com/&quot;&gt;CloudAMQP&lt;/a&gt;, a managed cloud hosting service for RabbitMQ.&lt;/p&gt;
&lt;p&gt;There are a few simple steps we need to follow in the CloudAMQP UI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click on &lt;strong&gt;Create New Instance&lt;/strong&gt; and select the free &lt;strong&gt;Loyal Lemming&lt;/strong&gt; plan.&lt;/li&gt;
&lt;li&gt;Select your region and data center (AWS is fine), and your instance is created!&lt;/li&gt;
&lt;li&gt;Click on your new instance to access the dashboard.&lt;/li&gt;
&lt;li&gt;In your dashboard overview, look for the &lt;strong&gt;AMQP details&lt;/strong&gt; and copy the &lt;strong&gt;AMQP URL&lt;/strong&gt;. It will appear partly redacted in the dashboard, so make sure you copy the whole thing! This URL is crucial: we will need it to create our Render blueprint in the next section.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Create Render Blueprint&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Go to &lt;a href=&quot;https://dashboard.render.com&quot;&gt;Render&lt;/a&gt; and create a new account (if you haven&amp;#39;t done so already).&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Blueprints&lt;/strong&gt; section and then click on &lt;strong&gt;+ New Blueprint Instance&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You should see a list of your GitHub repositories: select the one you want to connect as a Blueprint.&lt;/li&gt;
&lt;li&gt;In the next screen, name your Blueprint, then under &lt;strong&gt;Specified configurations&lt;/strong&gt;, add your AMQP URL as the value associated with the &lt;code&gt;CELERY_BROKER_URL&lt;/code&gt; key for both the web service and the background worker.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy Blueprint&lt;/strong&gt;. This will start the sync/deploy process.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lastly, enter your new Render-generated URL (and don&amp;#39;t forget the &lt;code&gt;/start-task&lt;/code&gt; endpoint). So the URL should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;https://[YOUR-RENDER-URL].onrender.com/start-task/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything works correctly, you will see the same behavior as when you run the project locally. After hitting the &lt;code&gt;start-task&lt;/code&gt; endpoint in your browser, go to &lt;strong&gt;Logs&lt;/strong&gt; in your celery-worker dashboard. You should see something very similar to the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;[2025-04-24 15:31:34,645: INFO/MainProcess] Task tasks.generate_report[87ca895f-debc-4d07-bdde-a3408faa8c7e] received
[2025-04-24 15:31:44,657: INFO/ForkPoolWorker-16] Task tasks.generate_report[87ca895f-debc-4d07-bdde-a3408faa8c7e] succeeded in 10.010491968001588s: &amp;#39;Report complete!&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Congratulations! You&amp;#39;ve made it to the end of this tutorial!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this post, we ran through setting up and deploying a Flask app to Render using Docker.&lt;/p&gt;
&lt;p&gt;First, we installed Docker on Mac (in the most painless way possible). Then, we created the local Docker version of our Flask app with &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;Dockerfile.worker&lt;/code&gt;, and &lt;code&gt;compose.yaml&lt;/code&gt; files. Lastly, we prepared our app for deployment to Render with &lt;code&gt;render.yaml&lt;/code&gt;, created a Render Blueprint, and a new CloudAMQP instance.&lt;/p&gt;
&lt;p&gt;If you would like to take things further and integrate your Render-deployed app with AppSignal, &lt;a href=&quot;https://docs.appsignal.com/logging/platforms/render&quot;&gt;check out AppSignal&amp;#39;s Render integration docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can also &lt;a href=&quot;https://docs.appsignal.com/standalone-agent/installation/docker-image.html&quot;&gt;check out the docs to use the AppSignal standalone agent as a Docker image&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How the Application and Request Contexts Work in Python Flask</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/07/23/how-the-application-and-request-contexts-work-in-flask.html"/>
    <id>https://blog.appsignal.com/2025/07/23/how-the-application-and-request-contexts-work-in-flask.html</id>
    <published>2025-07-23T00:00:00+00:00</published>
    <updated>2025-07-23T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll dive into contexts in Flask using some practical examples.</summary>
    <content type="html">&lt;p&gt;If you have spent some time developing Flask applications, you have probably encountered terms like &lt;code&gt;request&lt;/code&gt;, &lt;code&gt;session&lt;/code&gt;, &lt;code&gt;current_app&lt;/code&gt;, and &lt;code&gt;g&lt;/code&gt;. You might even use them daily. But have you ever stopped to think about how Flask makes these seemingly global objects available exactly when you need them, especially in a multi-threaded web server environment? Well, the magic lies in Flask&amp;#39;s context system.&lt;/p&gt;
&lt;p&gt;In this article, you will learn what contexts are in Flask and how to use them with practical examples.&lt;/p&gt;
&lt;p&gt;Let’s dive in!&lt;/p&gt;
&lt;h2&gt;What Are Contexts in Flask?&lt;/h2&gt;
&lt;p&gt;In web frameworks, a context refers to a set of information relevant to the current state of processing. Think of it as a temporary workspace that holds all the tools and data needed for a specific task.&lt;/p&gt;
&lt;p&gt;Why does Flask bother with this? Imagine a busy web server handling multiple user requests simultaneously. If objects like the current request details were truly global variables, different threads handling different requests could overwrite each other&amp;#39;s data, leading to chaos!&lt;/p&gt;
&lt;p&gt;Flask uses contexts to manage access to objects, such as the application instance and incoming request data, in a way that is thread-safe. Each thread gets its own version of the context, ensuring data isolation.&lt;/p&gt;
&lt;p&gt;Flask primarily uses two types of contexts, which often work together:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Application Context:&lt;/strong&gt; Deals with application-level data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request Context:&lt;/strong&gt; Deals with data specific to a single incoming HTTP request.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;#39;s break down each one.&lt;/p&gt;
&lt;h2&gt;The Application Context: The App&amp;#39;s Workspace&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://flask.palletsprojects.com/en/stable/appcontext/&quot;&gt;application context&lt;/a&gt; is Flask&amp;#39;s way of keeping track of application-level information that is not tied to a specific user request. Its main job is to make sure certain objects are accessible when needed, primarily the application instance itself.&lt;/p&gt;
&lt;p&gt;When an application context is active, Flask makes available the following key objects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.current_app&quot;&gt;&lt;code&gt;current_app&lt;/code&gt;&lt;/a&gt;: This is a proxy object that points to the Flask application instance handling the current activity. It allows your blueprints, extensions, and other modules to access the app configuration, registered routes, etc., without needing you to pass the &lt;code&gt;app&lt;/code&gt; object around explicitly.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.g&quot;&gt;&lt;code&gt;g&lt;/code&gt;&lt;/a&gt;: It stands for &amp;#39;global&amp;#39;, though its scope is context-bound. Think of it as a temporary scratchpad. It is a namespace object where you can store arbitrary data during the life of an application context. A common use case is storing a database connection or the logged-in user details once per request, making them easily accessible elsewhere in your code during that same request handling cycle. However, consider that it only holds data for the duration of a single request. So, if you need to store user data that persists between a request, you must use other objects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Flask automatically activates an application context before handling a request and deactivates it afterward. This usually happens hand-in-hand with the request context. You can also manually push an application context using the method &lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.current_app&quot;&gt;&lt;code&gt;push()&lt;/code&gt;&lt;/a&gt;. This is essential when you need access to &lt;code&gt;current_app&lt;/code&gt; or &lt;code&gt;g&lt;/code&gt; outside of a typical request cycle, like in background jobs, cron tasks, or Flask CLI commands.&lt;/p&gt;
&lt;h2&gt;The Request Context: Handling Individual Requests&lt;/h2&gt;
&lt;p&gt;While the application context deals with the app, the &lt;a href=&quot;https://flask.palletsprojects.com/en/stable/reqcontext/&quot;&gt;request context&lt;/a&gt; is all about a single incoming HTTP request from a client. It holds everything Flask needs to know to process that specific interaction.&lt;/p&gt;
&lt;p&gt;The key objects available when a request context is active are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.request&quot;&gt;&lt;code&gt;request&lt;/code&gt;&lt;/a&gt;: This is probably the most frequently used context object. It is a proxy representing the incoming HTTP request. Through it, you can access form data (&lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.Request.form&quot;&gt;&lt;code&gt;request.form&lt;/code&gt;&lt;/a&gt;), query parameters (&lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.Request.args&quot;&gt;&lt;code&gt;request.args&lt;/code&gt;&lt;/a&gt;), uploaded files (&lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.Request.files&quot;&gt;&lt;code&gt;request.files&lt;/code&gt;&lt;/a&gt;), headers (&lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.Request.headers&quot;&gt;&lt;code&gt;request.headers&lt;/code&gt;&lt;/a&gt;), and much more. It is your window into what the client is asking for.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flask.palletsprojects.com/en/stable/api/#flask.session&quot;&gt;&lt;code&gt;session&lt;/code&gt;&lt;/a&gt;: This object allows you to store user-specific information across multiple requests. Flask&amp;#39;s default session implementation uses cryptographically signed cookies. When you store something in &lt;code&gt;session&lt;/code&gt; (e.g., &lt;code&gt;session[&amp;quot;user_id&amp;quot;] = user.id&lt;/code&gt;), Flask serializes it, signs it, and sends it back to the client as a cookie. On subsequent requests, Flask reads the cookie, verifies the signature, and makes the data available again via the &lt;code&gt;session&lt;/code&gt; object. It is perfect for remembering who a user is between page loads.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Flask automatically pushes a request context right when it starts processing an incoming HTTP request and closes it when the response is generated and sent. Pushing a request context implicitly pushes an application context if one is not already active. This makes sense as you can not handle a request without knowing which application it belongs to. You generally do not need to manage the request context manually unless you are doing advanced testing or working with WebSockets outside the standard request-response flow.&lt;/p&gt;
&lt;h2&gt;Practical Examples&lt;/h2&gt;
&lt;p&gt;Understanding Flask contexts conceptually is one thing, but seeing them in action (and seeing things break when they&amp;#39;re missing) really drives the point home. This section provides code examples to demonstrate:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How &lt;code&gt;current_app&lt;/code&gt;, &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;request&lt;/code&gt;, and &lt;code&gt;session&lt;/code&gt; work within a standard request.&lt;/li&gt;
&lt;li&gt;What happens when you try to access these objects outside a request context.&lt;/li&gt;
&lt;li&gt;How to manually create contexts (&lt;code&gt;app_context&lt;/code&gt; and &lt;code&gt;test_request_context&lt;/code&gt;) to make these objects available when needed outside the normal request flow.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;To replicate this tutorial using Flask, you need to have at least &lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.6&lt;/a&gt; or newer installed on your machine.&lt;/p&gt;
&lt;h3&gt;Repository Structure and Requirements&lt;/h3&gt;
&lt;p&gt;Suppose you call the main folder of your project &lt;code&gt;flask_contexts&lt;/code&gt;. At the end of this step, the folder will have the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;flask_contexts/
   ├── venv/
   ├── app.py
   └── app_no_context.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;venv&lt;/code&gt; contains the virtual environment.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.py&lt;/code&gt; contains the code that manages the contexts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app_no_context.py&lt;/code&gt; contains the code that simulates a call outside the contexts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can create the &lt;code&gt;venv&lt;/code&gt; &lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;virtual environment&lt;/a&gt; directory like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;python -m venv venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To activate it on Windows, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or on macOS and Linux, execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the activated virtual environment, install the dependencies with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip install flask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wonderful! You now have what you need to create a Flask application using contexts.&lt;/p&gt;
&lt;h3&gt;Step 1: Basic Setup and App Initialization&lt;/h3&gt;
&lt;p&gt;As a first step, you need to set the application up in the &lt;code&gt;app.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
import secrets
from flask import Flask, request, session, g, current_app, jsonify

# Application Setup
app = Flask(__name__)
app.secret_key = secrets.token_hex(16)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this part of the code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app = Flask(__name__)&lt;/code&gt;: Creates an instance of the Flask application, where &lt;code&gt;__name__&lt;/code&gt; helps Flask determine the root path for resources.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.secret_key = secrets.token_hex(16)&lt;/code&gt;: Sets a secret key that generates a random 32-character hexadecimal string, suitable for a development environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 2: Define a Function That Needs Context&lt;/h3&gt;
&lt;p&gt;Define a Python function that attempts to access context-bound objects (&lt;code&gt;current_app&lt;/code&gt;, &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;request&lt;/code&gt;, and &lt;code&gt;session&lt;/code&gt;). This function is designed to work correctly if called when the necessary contexts are active, but it will raise a &lt;code&gt;RuntimeError&lt;/code&gt; if called when they are not:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def function_potentially_outside_context():
    &amp;quot;&amp;quot;&amp;quot;
    Tries to access context-bound objects.
    This will FAIL if called outside an active context.
    &amp;quot;&amp;quot;&amp;quot;
    try:
        # These require an active Application Context
        app_name = current_app.name
        g.some_value = &amp;quot;Set value in g&amp;quot;

        # These require an active Request Context (which includes App Context)
        method = request.method
        session_val = session.get(&amp;quot;example&amp;quot;, &amp;quot;Not Set&amp;quot;)

        return (f&amp;quot;SUCCESS (Inside Context): App=&amp;#39;{app_name}&amp;#39;, Method=&amp;#39;{method}&amp;#39;, &amp;quot;
                f&amp;quot;Session=&amp;#39;{session_val}&amp;#39;, g.some_value=&amp;#39;{g.some_value}&amp;#39;&amp;quot;)

    except RuntimeError as e:
        # This is the typical error when accessing context objects outside a context
        return f&amp;quot;FAILED: Caught RuntimeError: {e}&amp;quot;
    except Exception as e:
        # Catch any other unexpected errors
        return f&amp;quot;FAILED: Caught unexpected Exception: {e}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this snippet, the &lt;code&gt;try-except&lt;/code&gt; block manages the context-sensitive operations. If any of &lt;code&gt;current_app&lt;/code&gt;, &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;request&lt;/code&gt;, or &lt;code&gt;session&lt;/code&gt; are accessed when their corresponding context is not active, Flask raises a &lt;code&gt;RuntimeError&lt;/code&gt;.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h3&gt;Step 3: Set a Standard Route&lt;/h3&gt;
&lt;p&gt;Create a standard Flask route. When a user visits the root URL, Flask automatically sets up both the Application and Request contexts before executing the &lt;code&gt;index&lt;/code&gt; function. Inside this function, you can freely use &lt;code&gt;current_app&lt;/code&gt;, &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;request&lt;/code&gt;, and &lt;code&gt;session&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;quot;/&amp;quot;)
def index():
    &amp;quot;&amp;quot;&amp;quot;
    Standard route where Flask handles contexts automatically.
    Demonstrates &amp;#39;g&amp;#39; reset and &amp;#39;session&amp;#39; persistence.
    &amp;quot;&amp;quot;&amp;quot;
    print(&amp;quot;\n---&amp;gt; Entering index route handler&amp;quot;)

    # Check if &amp;#39;g&amp;#39; has data from a previous request
    g_before = getattr(g, &amp;quot;request_timestamp&amp;quot;, &amp;quot;Not Set Before This Request&amp;quot;)
    print(f&amp;quot;Value of g.request_timestamp before setting: {g_before}&amp;quot;)

    # Set a value in &amp;#39;g&amp;#39; for this specific request
    g.request_timestamp = time.time()
    print(f&amp;quot;Set g.request_timestamp: {g.request_timestamp}&amp;quot;)

    # Increment a session counter
    visit_count = session.get(&amp;quot;visit_count&amp;quot;, 0) + 1
    session[&amp;quot;visit_count&amp;quot;] = visit_count
    print(f&amp;quot;Updated session[&amp;#39;visit_count&amp;#39;]: {visit_count}&amp;quot;)

    # Access other context objects
    app_name = current_app.name
    req_method = request.method
    print(f&amp;quot;Accessed current_app.name: {app_name}&amp;quot;)
    print(f&amp;quot;Accessed request.method: {req_method}&amp;quot;)

    # Simulate work and use &amp;#39;g&amp;#39; within the same request
    time.sleep(0.05)
    duration = time.time() - g.request_timestamp
    print(f&amp;quot;Request processing duration (using g): {duration:.4f}s&amp;quot;)

    print(&amp;quot;&amp;lt;--- Exiting index route handler&amp;quot;)
    return jsonify({
        &amp;quot;message&amp;quot;: &amp;quot;Inside standard request - contexts handled automatically.&amp;quot;,
        &amp;quot;app_name&amp;quot;: app_name,
        &amp;quot;request_method&amp;quot;: req_method,
        &amp;quot;g_timestamp_set_in_this_request&amp;quot;: g.request_timestamp,
        &amp;quot;session_visit_count&amp;quot;: visit_count,
        &amp;quot;processing_duration_seconds&amp;quot;: f&amp;quot;{duration:.4f}&amp;quot;
    })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This route does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/functions.html#getattr&quot;&gt;&lt;code&gt;getattr()&lt;/code&gt;&lt;/a&gt;: Safely checks if &lt;code&gt;g&lt;/code&gt; has the attribute &lt;code&gt;request_timestamp&lt;/code&gt; from this request&amp;#39;s context setup. This shows &lt;code&gt;g&lt;/code&gt; is fresh for each request.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;time.time()&lt;/code&gt;: Stores the current time in &lt;code&gt;g&lt;/code&gt;. This value is only available during this specific request.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;session.get() + 1&lt;/code&gt;: Retrieves &lt;code&gt;visit_count&lt;/code&gt; from the session (defaulting to 0 if not found) and increments it at each new visit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;session[&amp;quot;visit_count&amp;quot;] = visit_count&lt;/code&gt;: Stores the new count back in the session. This data will persist across requests for the same user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;current_app.name&lt;/code&gt;, &lt;code&gt;request.method&lt;/code&gt;: Provide direct access works because Flask sets up the contexts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jsonify()&lt;/code&gt;: Converts the Python dictionary into a JSON response for the browser.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 4: Call the Helper Function Within a Request&lt;/h3&gt;
&lt;p&gt;Create another route that calls the helper function from &lt;a href=&quot;#step-2-define-a-function-that-needs-context&quot;&gt;Step 2&lt;/a&gt;. Because this call happens inside a route handler where Flask has already set up the contexts, the helper function will succeed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;quot;/call-function-directly&amp;quot;)
def call_function_directly():
    &amp;quot;&amp;quot;&amp;quot;
    Calls the helper function directly from within a request.
    Contexts are already active, so it should succeed.
    &amp;quot;&amp;quot;&amp;quot;
    print(&amp;quot;\n---&amp;gt; Entering call_function_directly route handler&amp;quot;)
    # Since we are inside a request handler, contexts are active.
    result = function_potentially_outside_context() # Call the helper
    print(f&amp;quot;Result from helper: {result}&amp;quot;)
    print(&amp;quot;&amp;lt;--- Exiting call_function_directly route handler&amp;quot;)
    return jsonify({&amp;quot;test&amp;quot;: &amp;quot;Calling function from within a route&amp;quot;, &amp;quot;result&amp;quot;: result})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this route, &lt;code&gt;result = function_potentially_outside_context()&lt;/code&gt; calls the function defined in Step 2. Since Flask ensures contexts are active here, the &lt;code&gt;try-except&lt;/code&gt; block inside the function succeeds.&lt;/p&gt;
&lt;h3&gt;Step 5: Simulate the &amp;quot;No Context&amp;quot; Problem&lt;/h3&gt;
&lt;p&gt;This step highlights when you encounter problems. Create a route that simulates what happens if you try to call &lt;code&gt;function_potentially_outside_context&lt;/code&gt; from outside Flask&amp;#39;s request handling (like in a separate script or background task). The route itself can not truly demonstrate the failure (because &lt;strong&gt;it is a request&lt;/strong&gt;), but this will be discussed later.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;quot;/call-function-no-context&amp;quot;)
def call_function_no_context_trigger():
    &amp;quot;&amp;quot;&amp;quot;
    Simulates calling the function where contexts might be missing.
    This route primarily serves to show the function&amp;#39;s output format.
    &amp;quot;&amp;quot;&amp;quot;
    print(&amp;quot;\n---&amp;gt; Entering call_function_no_context_trigger route handler&amp;quot;)
    # Simulate the call outside of a request context
    result = &amp;quot;Simulated Failure: Imagine &amp;#39;RuntimeError: Working outside of application context.&amp;#39; was raised here.&amp;quot;
    print(&amp;quot;&amp;lt;--- Exiting call_function_no_context_trigger route handler&amp;quot;)
    return jsonify({
        &amp;quot;test&amp;quot;: &amp;quot;Simulating call without context (via route)&amp;quot;,
        &amp;quot;result&amp;quot;: result,
        &amp;quot;note&amp;quot;: &amp;quot;To see real failure, run function_potentially_outside_context() from a plain script.&amp;quot;
        })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function will be invoked in &lt;code&gt;app_no_context.py&lt;/code&gt; next to show the actual simulation outside the contexts.&lt;/p&gt;
&lt;h3&gt;Step 6: Running the Application&lt;/h3&gt;
&lt;p&gt;To run the &lt;code&gt;app.py&lt;/code&gt; application, write the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if __name__ == &amp;quot;__main__&amp;quot;:
    print(&amp;quot;\nStep 8: Starting Flask development server...&amp;quot;)
    print(&amp;quot;Access the examples at:&amp;quot;)
    print(&amp;quot;- http://127.0.0.1:5000/&amp;quot;)
    print(&amp;quot;- http://127.0.0.1:5000/call-function-directly&amp;quot;)
    print(&amp;quot;- http://127.0.0.1:5000/call-function-no-context&amp;quot;)
    print(&amp;quot;\nRun &amp;#39;python test_no_context.py&amp;#39; in another terminal to see the context error.&amp;quot;)
    app.run(debug=True, host=&amp;#39;0.0.0.0&amp;#39;, port=5000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Perfect! Now your &lt;code&gt;app.py&lt;/code&gt; file is ready to be tested!&lt;/p&gt;
&lt;h3&gt;Step 7: Manage the Request Outside the Context&lt;/h3&gt;
&lt;p&gt;To manage the request outside the context, write the following in the &lt;code&gt;app_no_context.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from app import app, function_potentially_outside_context

print(&amp;quot;Attempting to call function outside Flask&amp;#39;s request handling...&amp;quot;)
result = function_potentially_outside_context()
print(f&amp;quot;Result: {result}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you are ready to also test the &lt;code&gt;app_no_context.py&lt;/code&gt; file.&lt;/p&gt;
&lt;h3&gt;Step 8: Testing&lt;/h3&gt;
&lt;p&gt;As a first test, try the main application by writing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what you will see when you access the URL &lt;code&gt;http://127.0.0.1:5000/&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-07/main-url.png&quot; alt=&quot;The main URL shows the context has been used in a Flask application by Federico Trotta&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The browser shows that you have been successful, and each time you refresh the page, the &lt;code&gt;session_visit_count&lt;/code&gt; will increase, as expected. Even the timestamp will be different, as it is managed by the main route.&lt;/p&gt;
&lt;p&gt;This is the result when you access the URL &lt;code&gt;http://127.0.0.1:5000/call-function-directly&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-07/manual-call-inside-contexts.png&quot; alt=&quot;The manual call inside contexts in Flask by Federico Trotta&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Because the function is called from within a route where Flask has already set up the contexts, the result is a SUCCESS message.&lt;/p&gt;
&lt;p&gt;The interesting part is to simulate the request externally from the contexts. To do so, open a new terminal and type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;python app_no_context.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will obtain the following result on the CLI:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-07/request-outside-contexts.png&quot; alt=&quot;A request outside contexts in Flask by Federico Trotta&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This demonstrates the necessity of a context, as the function has been called outside of it, on purpose.&lt;/p&gt;
&lt;h3&gt;Step 9: Put it All Together&lt;/h3&gt;
&lt;p&gt;Below is what the &lt;code&gt;app.py&lt;/code&gt; file should now contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
import secrets
from flask import Flask, request, session, g, current_app, jsonify

# Application Setup
app = Flask(__name__)
app.secret_key = secrets.token_hex(16)


def function_potentially_outside_context():
    &amp;quot;&amp;quot;&amp;quot;
    Tries to access context-bound objects.
    This will FAIL if called outside an active context.
    &amp;quot;&amp;quot;&amp;quot;
    try:
        # These require an active Application Context
        app_name = current_app.name
        g.some_value = &amp;quot;Set value in g&amp;quot;

        # These require an active Request Context (which includes App Context)
        method = request.method
        session_val = session.get(&amp;quot;example&amp;quot;, &amp;quot;Not Set&amp;quot;)

        return (f&amp;quot;SUCCESS (Inside Context): App=&amp;#39;{app_name}&amp;#39;, Method=&amp;#39;{method}&amp;#39;, &amp;quot;
                f&amp;quot;Session=&amp;#39;{session_val}&amp;#39;, g.some_value=&amp;#39;{g.some_value}&amp;#39;&amp;quot;)

    except RuntimeError as e:
        # This is the typical error when accessing context objects outside a context
        return f&amp;quot;FAILED: Caught RuntimeError: {e}&amp;quot;
    except Exception as e:
        # Catch any other unexpected errors
        return f&amp;quot;FAILED: Caught unexpected Exception: {e}&amp;quot;


@app.route(&amp;quot;/&amp;quot;)
def index():
    &amp;quot;&amp;quot;&amp;quot;
    Standard route where Flask handles contexts automatically.
    Demonstrates &amp;#39;g&amp;#39; reset and &amp;#39;session&amp;#39; persistence.
    &amp;quot;&amp;quot;&amp;quot;
    print(&amp;quot;\n---&amp;gt; Entering index route handler&amp;quot;)

    # Check if &amp;#39;g&amp;#39; has data from a *previous* request (it shouldn&amp;#39;t)
    g_before = getattr(g, &amp;quot;request_timestamp&amp;quot;, &amp;quot;Not Set Before This Request&amp;quot;)
    print(f&amp;quot;Value of g.request_timestamp before setting: {g_before}&amp;quot;)

    # Set a value in &amp;#39;g&amp;#39; for this specific request
    g.request_timestamp = time.time()
    print(f&amp;quot;Set g.request_timestamp: {g.request_timestamp}&amp;quot;)

    # Increment a session counter
    visit_count = session.get(&amp;quot;visit_count&amp;quot;, 0) + 1
    session[&amp;quot;visit_count&amp;quot;] = visit_count
    print(f&amp;quot;Updated session[&amp;#39;visit_count&amp;#39;]: {visit_count}&amp;quot;)

    # Access other context objects
    app_name = current_app.name
    req_method = request.method
    print(f&amp;quot;Accessed current_app.name: {app_name}&amp;quot;)
    print(f&amp;quot;Accessed request.method: {req_method}&amp;quot;)

    # Simulate work and use &amp;#39;g&amp;#39; within the same request
    time.sleep(0.05)
    duration = time.time() - g.request_timestamp
    print(f&amp;quot;Request processing duration (using g): {duration:.4f}s&amp;quot;)

    print(&amp;quot;&amp;lt;--- Exiting index route handler&amp;quot;)
    return jsonify({
        &amp;quot;message&amp;quot;: &amp;quot;Inside standard request - contexts handled automatically.&amp;quot;,
        &amp;quot;app_name&amp;quot;: app_name,
        &amp;quot;request_method&amp;quot;: req_method,
        &amp;quot;g_timestamp_set_in_this_request&amp;quot;: g.request_timestamp,
        &amp;quot;session_visit_count&amp;quot;: visit_count,
        &amp;quot;processing_duration_seconds&amp;quot;: f&amp;quot;{duration:.4f}&amp;quot;
    })

@app.route(&amp;quot;/call-function-directly&amp;quot;)
def call_function_directly():
    &amp;quot;&amp;quot;&amp;quot;
    Calls the helper function directly from within a request.
    Contexts are already active, so it should succeed.
    &amp;quot;&amp;quot;&amp;quot;
    print(&amp;quot;\n---&amp;gt; Entering call_function_directly route handler&amp;quot;)
    # Since you are inside a request handler, contexts are active.
    result = function_potentially_outside_context()
    print(&amp;quot;&amp;lt;--- Exiting call_function_directly route handler&amp;quot;)
    return jsonify({&amp;quot;test&amp;quot;: &amp;quot;Calling function from within a route&amp;quot;, &amp;quot;result&amp;quot;: result})

@app.route(&amp;quot;/call-function-no-context&amp;quot;)
def call_function_no_context_trigger():
    &amp;quot;&amp;quot;&amp;quot;
    Simulates calling the function where contexts might be missing.
    This route primarily serves to show the function&amp;#39;s output format.
    &amp;quot;&amp;quot;&amp;quot;
    print(&amp;quot;\n---&amp;gt; Entering call_function_no_context_trigger route handler&amp;quot;)
    # Simulate the call outside of a request context
    result = &amp;quot;Simulated Failure: Imagine &amp;#39;RuntimeError: Working outside of application context.&amp;#39; was raised here.&amp;quot;
    print(&amp;quot;&amp;lt;--- Exiting call_function_no_context_trigger route handler&amp;quot;)
    return jsonify({
        &amp;quot;test&amp;quot;: &amp;quot;Simulating call without context (via route)&amp;quot;,
        &amp;quot;result&amp;quot;: result,
        &amp;quot;note&amp;quot;: &amp;quot;To see real failure, run function_potentially_outside_context() from a plain script.&amp;quot;
        })

if __name__ == &amp;quot;__main__&amp;quot;:
    print(&amp;quot;Starting Flask development server...&amp;quot;)
    print(&amp;quot;Access the examples at:&amp;quot;)
    print(&amp;quot;- http://127.0.0.1:5000/&amp;quot;)
    print(&amp;quot;- http://127.0.0.1:5000/call-function-directly&amp;quot;)
    print(&amp;quot;- http://127.0.0.1:5000/call-function-no-context&amp;quot;)
    app.run(debug=True, host=&amp;#39;0.0.0.0&amp;#39;, port=5000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Discussion About the &amp;quot;No Context&amp;quot; Simulation&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;app_no_context.py&lt;/code&gt; example shows that things break, but not when you could encounter it naturally, because you simulated it—somehow forcing it.&lt;/p&gt;
&lt;p&gt;So, when can the &lt;code&gt;app_no_context.py&lt;/code&gt; scenario happen in real life?&lt;/p&gt;
&lt;p&gt;The automatic context setup only happens during a live web request. There are many situations where you might want to run parts of your Flask application&amp;#39;s code outside of a direct web request. This is where you need to manually create contexts.&lt;/p&gt;
&lt;p&gt;Here are common real-world examples where you would be &amp;quot;outside&amp;quot; a request and need contexts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Background jobs/task queues:&lt;/strong&gt; A user uploads a video (web request). Your view function adds a task to a queue (like Celery) to process the video later. While the trigger to add a task to the queue might come from an HTTP request handled by Flask, the actual execution of the task happens later in a completely separate worker process. This worker process is not the same process as the Flask web server handling requests. It simply pulls a job description from a queue (like Redis or RabbitMQ) and runs the associated Python code. Since this worker process is not directly handling an incoming HTTP request, Flask doesn&amp;#39;t automatically set up any contexts for it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Flask CLI commands:&lt;/strong&gt; You write a command like &lt;code&gt;flask create-admin --email admin@example.com&lt;/code&gt; to add an admin user directly from your terminal. In this case, you are executing a Python script via the Flask command-line interface runner. There is no incoming HTTP request involved. You are interacting directly with the script on the command line. Because there&amp;#39;s no web request being processed, Flask&amp;#39;s automatic request-response context setup mechanism does not run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduled tasks:&lt;/strong&gt; You have a Python script run by &lt;code&gt;cron&lt;/code&gt; every night to clean up old user sessions or generate reports. Similarly to CLI commands, this execution happens independently of any web server or incoming HTTP request. Since Flask is not involved in handling an HTTP request at that moment, it doesn&amp;#39;t automatically create the contexts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, you learned that Flask&amp;#39;s context system is a fundamental mechanism that ensures application and request data are managed correctly and safely, especially in concurrent environments. By distinguishing between the Application Context and the Request Context, Flask offers a robust way to handle state.&lt;/p&gt;
&lt;p&gt;As demonstrated, attempting to access context-bound objects outside of an active context leads to a &lt;code&gt;RuntimeError&lt;/code&gt;. Recognizing when these situations arise allows developers to manually push the necessary contexts.&lt;/p&gt;
&lt;p&gt;Mastering Flask contexts allows you to write more reliable, maintainable, and testable applications, enabling you to leverage Flask&amp;#39;s full potential beyond simple request handling.&lt;/p&gt;
&lt;p&gt;Happy testing!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to Avoid N+1 Queries in Django Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/07/09/how-to-avoid-nplus1-queries-in-django-python.html"/>
    <id>https://blog.appsignal.com/2025/07/09/how-to-avoid-nplus1-queries-in-django-python.html</id>
    <published>2025-07-09T00:00:00+00:00</published>
    <updated>2025-07-09T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll see what N+1 queries are, why they can be an issue for your application, and how to mitigate them using Django’s best practices.</summary>
    <content type="html">&lt;p&gt;Django is a powerful web framework that simplifies how developers interact with databases through its Object-Relational Mapping (ORM) system. However, even with its benefits, it’s easy to fall into performance pitfalls such as the N+1 query problem.&lt;/p&gt;
&lt;p&gt;In this article, we’ll explore what N+1 queries are, why they can be an issue for your application, and how to mitigate them using Django’s best practices.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s dive in!&lt;/p&gt;
&lt;h2&gt;Understanding the N+1 Query Problem in Django&lt;/h2&gt;
&lt;p&gt;Before diving into solutions, it’s essential to understand the fundamentals of this performance bottleneck.&lt;/p&gt;
&lt;p&gt;Imagine you’re creating a Django application that needs to display a list of authors and their corresponding books. You might write a simple query to retrieve all the authors, and then iterate over each one to pull their books. At first glance, this appears straightforward. However, if your application makes a separate database query for each author’s books, you might end up issuing many more queries than you intended: a classic symptom of the N+1 query problem.&lt;/p&gt;
&lt;p&gt;In Django, N+1 queries occur when one initial query (the “1”) is followed by additional queries (the “N”) for each object retrieved. This scenario usually surfaces when code is structured in a way that iterates over many objects and lazily retrieves related data. The result can be significant performance degradation when interacting with a database. For example, if you have 100 authors and you access each author’s books in a loop, Django could run 101 queries in total, rather than a single, optimized query. This pattern not only increases latency, but also puts unnecessary load on your database server.&lt;/p&gt;
&lt;p&gt;The impact of N+1 queries on database performance can be substantial. As the number of objects grows, so does the number of queries, leading to a linear increase in database calls. This kind of inefficiency can slow down page load times and strain your application, especially when your database scales or when operating in a high-traffic environment. This issue brings to light the importance of querying data as efficiently as possible and leveraging Django’s abilities to ensure optimized performance.&lt;/p&gt;
&lt;h2&gt;How N+1 Queries Arise in Django&lt;/h2&gt;
&lt;p&gt;The N+1 problem typically appears in scenarios where developers do not explicitly optimize the way related objects are queried. Two common patterns that lead to this pitfall are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accessing related fields in templates&lt;/strong&gt;:
Django’s ORM performs lazy loading, which means that when you reference a related field in a template, Django may execute a separate query for each object accessed. For example, if you loop through a list of articles in a template and display each article’s author, Django might issue an extra query for each article to fetch the corresponding author details. This situation leads to N+1 queries because one query retrieves the articles, and each article then triggers another query for its associated author.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Iterating over related objects in views&lt;/strong&gt;:
A second common pattern occurs in view logic, particularly when iterating over a &lt;a href=&quot;https://docs.djangoproject.com/en/5.1/ref/models/querysets/&quot;&gt;&lt;code&gt;queryset&lt;/code&gt;&lt;/a&gt; and accessing related objects without preloading. Consider a scenario where you retrieve a list of authors and then, within a loop in your view, access each author’s set of books. If you don&amp;#39;t optimize your query, Django will execute a separate query for each author to fetch their books. This repetition causes query numbers to proliferate as the number of authors grows, resulting in an N+1 query scenario.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How to Avoid N+1 Queries&lt;/h2&gt;
&lt;p&gt;Now that we&amp;#39;ve gone through the theory, it is time to provide a step-by-step tutorial. This section first shows how N+1 queries look, then two ways of solving the issue using Django.&lt;/p&gt;
&lt;p&gt;We will use Django to retrieve a list of authors and related books from a blog.&lt;/p&gt;
&lt;h2&gt;Requirements&lt;/h2&gt;
&lt;p&gt;To replicate the tutorial, you must have &lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.10.1&lt;/a&gt; or higher installed on your machine.&lt;/p&gt;
&lt;h3&gt;Prerequisites and Dependencies&lt;/h3&gt;
&lt;p&gt;Suppose you call the main folder of your project &lt;code&gt;django_queries/&lt;/code&gt;. In the beginning, the folder should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;django_queries/
    └── venv/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;venv/&lt;/code&gt; contains the virtual environment. You can create the &lt;a href=&quot;https://docs.python.org/3/library/venv.htmldirectory&quot;&gt;&lt;code&gt;venv/&lt;/code&gt; virtual environment&lt;/a&gt; like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m venv venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To activate it on Windows, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On macOS and Linux, execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the activated virtual environment, install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install django
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 1: Create a New Django Project&lt;/h3&gt;
&lt;p&gt;Start a new project like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;django-admin startproject nplus1_project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will automatically create subfolders with Python files. Go to the &lt;code&gt;nplus1_project/&lt;/code&gt; subfolder:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd nplus1_project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start a new Django application called blog:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py startapp blog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The structure of your repository should now look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;nplus1_project/
    ├── blog/
    │   ├── migrations/
    │       └── __init__.py
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    ├── nplus1_project/
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── manage.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Define Models&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;N+1&lt;/code&gt; query problem typically arises when querying related models. So, let’s create two models: &lt;code&gt;Author&lt;/code&gt; and &lt;code&gt;Post&lt;/code&gt;, where each &lt;code&gt;Post&lt;/code&gt; is written by an &lt;code&gt;Author&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Define the models in the &lt;code&gt;blog/models.py&lt;/code&gt; file, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name=&amp;#39;posts&amp;#39;)

    def __str__(self):
        return self.title
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply the migrations to Django by running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py makemigrations
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you&amp;#39;ve created the basis of this tutorial.&lt;/p&gt;
&lt;h3&gt;Step 3: Populate the Database&lt;/h3&gt;
&lt;p&gt;The next step is to populate the database with some data. Open the Django shell:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following data to the database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from blog.models import Author, Post

# Create authors
author1 = Author.objects.create(name=&amp;quot;Author 1&amp;quot;)
author2 = Author.objects.create(name=&amp;quot;Author 2&amp;quot;)

# Create posts
Post.objects.create(title=&amp;quot;Post 1&amp;quot;, content=&amp;quot;Content 1&amp;quot;, author=author1)
Post.objects.create(title=&amp;quot;Post 2&amp;quot;, content=&amp;quot;Content 2&amp;quot;, author=author1)
Post.objects.create(title=&amp;quot;Post 3&amp;quot;, content=&amp;quot;Content 3&amp;quot;, author=author2)
Post.objects.create(title=&amp;quot;Post 4&amp;quot;, content=&amp;quot;Content 4&amp;quot;, author=author2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you encounter an error at this stage that says something like &lt;code&gt;&amp;quot;NameError: name &amp;#39;Post&amp;#39; is not defined&amp;quot;&lt;/code&gt;, it means that Django doesn&amp;#39;t recognize your blog application or its models. To fix this, go into the &lt;code&gt;settings.py&lt;/code&gt; file inside the &lt;code&gt;nplus1_project&lt;/code&gt; directory, search for the &lt;code&gt;INSTALLED_APPS&lt;/code&gt; list and add your &lt;code&gt;blog&lt;/code&gt; app name, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;INSTALLED_APPS = [
    &amp;#39;django.contrib.admin&amp;#39;,
    &amp;#39;django.contrib.auth&amp;#39;,
    &amp;#39;django.contrib.contenttypes&amp;#39;,
    &amp;#39;django.contrib.sessions&amp;#39;,
    &amp;#39;django.contrib.messages&amp;#39;,
    &amp;#39;django.contrib.staticfiles&amp;#39;,
    &amp;#39;blog&amp;#39;,  # Add your app here
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the &lt;code&gt;settings.py&lt;/code&gt; file and apply the migrations again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py makemigrations
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Create a View with the N+1 Query Problem&lt;/h3&gt;
&lt;p&gt;In this step, we&amp;#39;ll actually create the N+1 query issue on purpose to help you understand how to visualize it. To do so, open &lt;code&gt;blog/views.py&lt;/code&gt; and create a view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.shortcuts import render
from blog.models import Post

def post_list(request):
    # This will cause the N+1 query problem
    posts = Post.objects.all()
    for post in posts:
        print(post.author.name)  # Accessing the related author for each post

    return render(request, &amp;#39;blog/post_list.html&amp;#39;, {&amp;#39;posts&amp;#39;: posts})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the &lt;code&gt;post_list()&lt;/code&gt; view queries all the &lt;code&gt;Post&lt;/code&gt; objects. Then, for each &lt;code&gt;Post&lt;/code&gt;, it accesses the related &lt;code&gt;Author&lt;/code&gt; object, causing a separate query for each &lt;code&gt;Post&lt;/code&gt;, thus creating the N+1 query issue.&lt;/p&gt;
&lt;h3&gt;Step 5: Create a URL for the View&lt;/h3&gt;
&lt;p&gt;Create a file called &lt;code&gt;urls.py&lt;/code&gt; in the folder &lt;code&gt;blog/&lt;/code&gt; and define the URL for the &lt;code&gt;post_list&lt;/code&gt; view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.urls import path
from . import views

urlpatterns = [
    path(&amp;#39;&amp;#39;, views.post_list, name=&amp;#39;post_list&amp;#39;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, set the &lt;code&gt;nplus1_project/urls.py&lt;/code&gt; file like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path(&amp;#39;admin/&amp;#39;, admin.site.urls),
    path(&amp;#39;&amp;#39;, include(&amp;#39;blog.urls&amp;#39;)),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 6: Create a Template&lt;/h3&gt;
&lt;p&gt;To visualize how data is retrieved and displayed by the UI, you need to create a template. Create a directory for the templates in &lt;code&gt;blog/templates/blog/&lt;/code&gt;. Inside it, create a file named &lt;code&gt;post_list.html&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Post List&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
    &amp;lt;ul&amp;gt;
      {% for post in posts %}
      &amp;lt;li&amp;gt;{{ post.title }} by {{ post.author.name }}&amp;lt;/li&amp;gt;
      {% endfor %}
    &amp;lt;/ul&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final repository structure looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;django_queries/
│    ├── blog/
│    │   ├── migrations/
│    │   ├── templates/
│    │   │   └── blog/
│    │   │       └── post_list.html
│    │   ├── __init__.py
│    │   ├── admin.py
│    │   ├── apps.py
│    │   ├── models.py
│    │   ├── tests.py
│    │   ├── urls.py
│    │   └── views.py
│    ├── nplus1_project/
│    │   ├── __init__.py
│    │   ├── asgi.py
│    │   ├── settings.py
│    │   ├── urls.py
│    │   └── wsgi.py
│    ├── db.sqlite3
│    └── manage.py
└── venv/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 7: Run The Application&lt;/h3&gt;
&lt;p&gt;In &lt;code&gt;nplus1_project/settings.py&lt;/code&gt;, enable the possibility to log SQL queries via the console by adding the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;LOGGING = {
    &amp;#39;version&amp;#39;: 1,
    &amp;#39;disable_existing_loggers&amp;#39;: False,
    &amp;#39;handlers&amp;#39;: {
        &amp;#39;console&amp;#39;: {
            &amp;#39;class&amp;#39;: &amp;#39;logging.StreamHandler&amp;#39;,
        },
    },
    &amp;#39;loggers&amp;#39;: {
        &amp;#39;django.db.backends&amp;#39;: {
            &amp;#39;level&amp;#39;: &amp;#39;DEBUG&amp;#39;,
            &amp;#39;handlers&amp;#39;: [&amp;#39;console&amp;#39;],
        },
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, run the server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go to &lt;a href=&quot;http://127.0.0.1:8000/&quot;&gt;http://127.0.0.1:8000/&lt;/a&gt; and see the server running. The listed data is displayed like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-07/list.png&quot; alt=&quot;The listed data from the n+1 query with Django&quot;/&gt;&lt;/p&gt;
&lt;p&gt;However, what matters to us is what is happening under the hood. Here is the expected output you will see on the CLI:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-07/nplus1-queries.png&quot; alt=&quot;The n+1 query with Django&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This is an N+1 query issue, as you can observe:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One query to fetch all &lt;code&gt;Post&lt;/code&gt; objects.&lt;/li&gt;
&lt;li&gt;Additional queries for each &lt;code&gt;Post&lt;/code&gt; to fetch its related &lt;code&gt;Author&lt;/code&gt; object.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, let&amp;#39;s see how to solve the issue.&lt;/p&gt;
&lt;h3&gt;Step 9: Fix the N+1 Query Problem With &lt;code&gt;select_related()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;One way to solve the N+1 query is to use the &lt;a href=&quot;https://docs.djangoproject.com/en/5.1/ref/models/querysets/#select-related&quot;&gt;&lt;code&gt;select_related()&lt;/code&gt;&lt;/a&gt; method. To do so, modify the &lt;code&gt;blog/views.py&lt;/code&gt; file like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def post_list(request):
    # Use select_related to avoid the N+1 query problem
    posts = Post.objects.select_related(&amp;#39;author&amp;#39;)
    for post in posts:
        print(post.author.name)  # No additional queries are executed here

    return render(request, &amp;#39;blog/post_list.html&amp;#39;, {&amp;#39;posts&amp;#39;: posts})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run the server again. This is the expected result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-07/select-related.png&quot; alt=&quot;Using select related for the n+1 query with Django&quot;/&gt;&lt;/p&gt;
&lt;p&gt;In this case, the query fetches all &lt;code&gt;Post&lt;/code&gt; objects, joins the &lt;code&gt;Author&lt;/code&gt; table using the &lt;code&gt;author_id&lt;/code&gt; foreign key, and retrieves all the necessary fields in a single query.&lt;/p&gt;
&lt;p&gt;Hooray! You fixed the problem!&lt;/p&gt;
&lt;h3&gt;Step 10: Fix the N+1 Query Problem With &lt;code&gt;prefetch_related()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Another way to solve this issue is to use the &lt;a href=&quot;https://docs.djangoproject.com/en/5.1/ref/models/querysets/#prefetch-related&quot;&gt;&lt;code&gt;prefetch_related()&lt;/code&gt;&lt;/a&gt; method. Modify the &lt;code&gt;blog/views.py&lt;/code&gt; file like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def post_list(request):
    # Use prefetch_related to avoid the N+1 query problem
    posts = Post.objects.prefetch_related(&amp;#39;author&amp;#39;)
    for post in posts:
        print(post.author.name)  # Accessing the related author for each post

    return render(request, &amp;#39;blog/post_list.html&amp;#39;, {&amp;#39;posts&amp;#39;: posts})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running the server again, this is the expected result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-07/prefetch-related.png&quot; alt=&quot;Using prefetch related for the n+1 query with Django&quot;/&gt;&lt;/p&gt;
&lt;p&gt;And again, the data is retrieved in a single query!&lt;/p&gt;
&lt;h2&gt;When to Use &lt;code&gt;prefetch_related()&lt;/code&gt; or &lt;code&gt;select_related()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;You may be wondering when to use &lt;code&gt;select_related()&lt;/code&gt; over &lt;code&gt;prefetch_related()&lt;/code&gt;. This depends on the type of relationship between your models and your specific use case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;select_related()&lt;/code&gt; is used for single-valued relationships, such as &lt;a href=&quot;https://docs.djangoproject.com/en/5.1/ref/models/fields/#foreignkey&quot;&gt;&lt;code&gt;ForeignKey&lt;/code&gt;&lt;/a&gt; or &lt;a href=&quot;https://docs.djangoproject.com/en/5.1/ref/models/fields/#onetoonefield&quot;&gt;&lt;code&gt;OneToOneField&lt;/code&gt;&lt;/a&gt;. It performs a single SQL query with a JOIN to fetch the related data.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prefetch_related()&lt;/code&gt; is used for multi-valued relationships, such as &lt;a href=&quot;https://docs.djangoproject.com/en/5.1/ref/models/fields/#manytomanyfield&quot;&gt;&lt;code&gt;ManyToManyField&lt;/code&gt;&lt;/a&gt; or reverse &lt;code&gt;ForeignKey&lt;/code&gt; relationships. It performs two separate queries and uses Python to match the related objects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is a summary table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;code&gt;select_related()&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;prefetch_related()&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single-valued relationships (&lt;code&gt;ForeignKey&lt;/code&gt;, &lt;code&gt;OneToOneField&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Multi-valued relationships (&lt;code&gt;ManyToManyField&lt;/code&gt;, reverse &lt;code&gt;ForeignKey&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Number of Queries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 query with &lt;code&gt;JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 queries (main query + prefetch query)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Faster for single-valued relationships&lt;/td&gt;
&lt;td&gt;Better for large datasets or multi-valued relationships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database Load&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;JOIN&lt;/code&gt;, which can be expensive for large datasets&lt;/td&gt;
&lt;td&gt;Separate queries, which can be more efficient for large datasets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Level Up Django Monitoring with AppSignal&lt;/h2&gt;
&lt;p&gt;If you are a Django user and want to level up your monitoring capabilities, consider trying &lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/django.html&quot;&gt;AppSignal&amp;#39;s integration for Django&lt;/a&gt;. To spot N+1 queries, AppSignal has an &lt;a href=&quot;https://docs.appsignal.com/appsignal/terminology.html#instrumentation-events&quot;&gt;instrumentation events feature&lt;/a&gt; that detects if the same event is repeated more than once in succession. Such events are marked as potential N+1 events and shown in the &lt;strong&gt;Performance&lt;/strong&gt; tab of your dashboard, under &lt;strong&gt;Issue list&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-07/performance.png&quot; alt=&quot;The performance tab in AppSignal for the n+1 query with Django&quot;/&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s it!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, you learned about N+1 queries in Django. First, we explored how N+1 queries come to exist in your application, before diving into how to solve them using &lt;code&gt;prefetch_related()&lt;/code&gt; and &lt;code&gt;select_related()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to Use MongoDB in Python Flask</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/07/02/how-to-use-mongodb-in-python-flask.html"/>
    <id>https://blog.appsignal.com/2025/07/02/how-to-use-mongodb-in-python-flask.html</id>
    <published>2025-07-02T00:00:00+00:00</published>
    <updated>2025-07-02T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s explore the benefits of MongoDB and use it in a Flask application.</summary>
    <content type="html">&lt;p&gt;When developing software applications, data storage is a key concern. The reality is that your first concern should be the data model you choose, which in turn affects how you store data. Generally speaking, this means deciding between SQL and NoSQL databases.&lt;/p&gt;
&lt;p&gt;In this article, you will learn how to use MongoDB, a popular NoSQL database, in a Flask application. First, you will learn why MongoDB is a good choice, and then we will implement a practical hands-on project using MongoDB in Flask.&lt;/p&gt;
&lt;h2&gt;Why Use MongoDB in a Python Flask Application?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.mongodb.com/&quot;&gt;MongoDB&lt;/a&gt; is an open-source document-oriented database management system (DBMS) classified as NoSQL, as it stores data in JSON-like documents (the so-called &lt;a href=&quot;https://www.mongodb.com/resources/languages/bson&quot;&gt;BSON&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;MongoDB was designed with scalability and high availability in mind, making it suitable for large-scale web applications. It also provides automatic sharding capabilities, allowing users to distribute their data across multiple servers without having to worry about complex configurations or manual partitioning processes.&lt;/p&gt;
&lt;p&gt;So, given its characteristics, here are some reasons why you might want to consider using MongoDB in your Flask applications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flexibility with data modeling&lt;/strong&gt;: Unlike the classical relational databases, MongoDB allows you to define flexible schemas for your collections, given its NoSQL nature. This means that it allows you to work with &lt;a href=&quot;https://www.mongodb.com/resources/basics/unstructured-data&quot;&gt;unstructured and semi-structured data&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: MongoDB supports horizontal scaling through &lt;a href=&quot;https://www.mongodb.com/resources/products/capabilities/sharding&quot;&gt;sharding&lt;/a&gt;, which enables distributing data across multiple servers while maintaining data consistency (all the shards form a sharded cluster, which acts as a single logical database). This allows you to handle increased loads to high degrees by increasing read/write throughput and storage capacity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy integration&lt;/strong&gt;: MongoDB integrates seamlessly with many programming languages and frameworks, including Python. In the case of Flask, for example, there are several libraries that make the integration process smooth, like &lt;a href=&quot;https://pymongo.readthedocs.io/en/stable/&quot;&gt;&lt;code&gt;pymongo&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://flask-pymongo.readthedocs.io/en/latest/&quot;&gt;&lt;code&gt;flask-pymongo&lt;/code&gt;&lt;/a&gt;, and &lt;a href=&quot;https://pythonhosted.org/Flask-MongoAlchemy/&quot;&gt;&lt;code&gt;Flask-MongoAlchemy&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time applications&lt;/strong&gt;: MongoDB provides the ability to manage real-time data streams efficiently by &lt;a href=&quot;https://www.mongodb.com/solutions/use-cases/analytics/real-time-analytics&quot;&gt;integrating seamlessly with other technologies&lt;/a&gt; such as &lt;a href=&quot;https://kafka.apache.org/&quot;&gt;Apache Kafka&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to Use MongoDB in Flask: A Step-by-step Guide&lt;/h2&gt;
&lt;p&gt;Now that you know why you should use MongoDB with Flask, let&amp;#39;s dive into a step-by-step tutorial on integrating it into your application. We&amp;#39;ll implement basic CRUD operations for a &amp;quot;User&amp;quot; model.&lt;/p&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Before starting, ensure that you have installed the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python&lt;/a&gt; version 3.x&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pip.pypa.io/en/stable/installation/&quot;&gt;pip&lt;/a&gt; package manager for Python&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/venv.html&quot;&gt;virtualenv&lt;/a&gt; module for creating isolated environments in Python projects&lt;/li&gt;
&lt;li&gt;Any recent version of &lt;a href=&quot;https://www.mongodb.com/try/download/community&quot;&gt;MongoDB&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: As a prerequisite, you also need to create a new database named &lt;code&gt;my_database&lt;/code&gt; in MongoDB: it will be used later in this tutorial. To do so:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Launch MongoDB (the command depends on your Operating System and version of MongoDB) after you have installed it.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;use my_database&lt;/code&gt; to create a new database named &lt;code&gt;my_database&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Insert data into a collection to make it usable. You can type something like: &lt;code&gt;db.my_collection.insertOne({ name: &amp;quot;John Doe&amp;quot;, age: 30 })&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 1: Define Project Structure&lt;/h3&gt;
&lt;p&gt;Supposing you call your main folder &lt;code&gt;my_flask_mongo_app&lt;/code&gt;. Create the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;my_flask_mongo_app/
│
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   └── config.py
│
├── venv/
│
└── run.py
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: The &lt;code&gt;venv&lt;/code&gt; directory will be created when you activate the virtual environment later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Step 2: Create The Virtual Environment and Install the Needed Libraries&lt;/h3&gt;
&lt;p&gt;Now, create the virtual environment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux/macOS:&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m venv venv
source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m venv venv
venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then install the needed libraries via &lt;code&gt;pip&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install flask flask_pymongo
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Configure The Flask Application&lt;/h3&gt;
&lt;p&gt;In the &lt;code&gt;app/&lt;/code&gt; folder, compile the Python files as follows.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;config.py&lt;/code&gt; type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

class Config:
    SECRET_KEY = os.urandom(24)
    MONGO_URI = &amp;quot;mongodb://localhost:27017/my_database&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file defines the configuration for the Flask application. In particular:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MONGODB_URI&lt;/code&gt; specifies the database name, host, and port for connecting to MongoDB. In this case, you&amp;#39;ll be using the local MongoDB server running on port 27017 and accessing &lt;code&gt;my_database&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SECRET_KEY&lt;/code&gt; secures sessions and other security-related features in Flask.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In &lt;code&gt;__init__.py&lt;/code&gt; write the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Flask
from flask_pymongo import PyMongo
from .config import Config

# create PyMongo instance at module level
mongo = PyMongo()

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    # Initialize mongo with the app
    mongo.init_app(app)

    from .routes import main
    app.register_blueprint(main)

    return app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file initializes the Flask application by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating a Flask instance and loading the configuration from &lt;code&gt;Config&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Initializing &lt;code&gt;PyMongo&lt;/code&gt;, which allows Flask to interact with MongoDB.&lt;/li&gt;
&lt;li&gt;Importing and registering the main blueprint from &lt;code&gt;routes.py&lt;/code&gt;, which contains the application&amp;#39;s routes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 4: Define MongoDB Models&lt;/h3&gt;
&lt;p&gt;In the &lt;code&gt;models.py&lt;/code&gt; file, write the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from . import mongo

class User:
    @staticmethod
    def create(username, email):
        user = {
            &amp;#39;username&amp;#39;: username,
            &amp;#39;email&amp;#39;: email
        }
        mongo.db.users.insert_one(user)
        return user

    @staticmethod
    def get_by_username(username):
        return mongo.db.users.find_one({&amp;#39;username&amp;#39;: username})

    @staticmethod
    def update(username, new_email):
        mongo.db.users.update_one(
            {&amp;#39;username&amp;#39;: username},
            {&amp;#39;$set&amp;#39;: {&amp;#39;email&amp;#39;: new_email}}
        )

    @staticmethod
    def delete(username):
        mongo.db.users.delete_one({&amp;#39;username&amp;#39;: username})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file defines the &amp;#39;User&amp;#39; model with static methods for CRUD operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;create()&lt;/code&gt; method inserts a new user document into the collection of users.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;get_by_username()&lt;/code&gt; method retrieves a user document by username.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;update()&lt;/code&gt; method updates a user&amp;#39;s email.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;delete()&lt;/code&gt; method deletes a user document by username.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 5: Implement Routes and CRUD Operations&lt;/h3&gt;
&lt;p&gt;Use the &lt;code&gt;routes.py&lt;/code&gt; file to create the actual CRUD operations via routes in Flask:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Blueprint, request, jsonify
from .models import User

main = Blueprint(&amp;#39;main&amp;#39;, __name__)

@main.route(&amp;#39;/users&amp;#39;, methods=[&amp;#39;POST&amp;#39;])
def create_user():
    data = request.json
    user = User.create(data[&amp;#39;username&amp;#39;], data[&amp;#39;email&amp;#39;])
    return jsonify(user), 201

@main.route(&amp;#39;/users/&amp;lt;username&amp;gt;&amp;#39;, methods=[&amp;#39;GET&amp;#39;])
def get_user(username):
    user = User.get_by_username(username)
    if user:
        return jsonify(user), 200
    return jsonify({&amp;#39;error&amp;#39;: &amp;#39;User not found&amp;#39;}), 404

@main.route(&amp;#39;/users/&amp;lt;username&amp;gt;&amp;#39;, methods=[&amp;#39;PUT&amp;#39;])
def update_user(username):
    data = request.json
    User.update(username, data[&amp;#39;email&amp;#39;])
    return jsonify({&amp;#39;message&amp;#39;: &amp;#39;User updated&amp;#39;}), 200

@main.route(&amp;#39;/users/&amp;lt;username&amp;gt;&amp;#39;, methods=[&amp;#39;DELETE&amp;#39;])
def delete_user(username):
    User.delete(username)
    return jsonify({&amp;#39;message&amp;#39;: &amp;#39;User deleted&amp;#39;}), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code defines the API endpoints (routes) for the Flask application. In particular, each route corresponds to a CRUD operation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /users&lt;/code&gt;: Creates a new user.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /users/&amp;lt;username&amp;gt;&lt;/code&gt;: Retrieves a user by username.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUT /users/&amp;lt;username&amp;gt;&lt;/code&gt;: Updates a user&amp;#39;s email.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /users/&amp;lt;username&amp;gt;&lt;/code&gt;: Deletes a user.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 7: Create and Run the Main Application File&lt;/h3&gt;
&lt;p&gt;To create the main application, write the following code into the &lt;code&gt;run.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from app import create_app

app = create_app()

if __name__ == &amp;#39;__main__&amp;#39;:
    app.run(debug=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file creates the Flask app and MongoDB connection using the &lt;code&gt;create_app()&lt;/code&gt; function from &lt;code&gt;app/__init__.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then, run the application by typing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 run.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While the application runs, you can test the API endpoints using tools like Postman or curl.&lt;/p&gt;
&lt;p&gt;For example, to create a new user, send a POST request to &lt;a href=&quot;http://localhost:5000/users&quot;&gt;http://localhost:5000/users&lt;/a&gt; with the following JSON payload:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;username&amp;quot;: &amp;quot;john_doe&amp;quot;,
  &amp;quot;email&amp;quot;: &amp;quot;johndoe@example.com&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can do it via curl like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST \
     -H &amp;quot;Content-Type: application/json&amp;quot; \
     -d &amp;#39;{&amp;quot;username&amp;quot;: &amp;quot;john_doe&amp;quot;, &amp;quot;email&amp;quot;: &amp;quot;john@example.com&amp;quot;}&amp;#39; \
     http://localhost:5000/users
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s all! Now you know the basics of how to use MongoDB in Flask.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we learned how to use MongoDB in Flask. First, we explored why and when you should use MongoDB. Then, we offered a step-by-step guide on using MongoDB in Flask that supports and implements basic CRUD operations for a &amp;quot;User&amp;quot; model.&lt;/p&gt;
&lt;p&gt;Oh, and by the way, did you know that AppSignal provides &lt;a href=&quot;https://docs.appsignal.com/nodejs/3.x/integrations/mongo.html&quot;&gt;MongoDB&lt;/a&gt; and &lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/flask.html&quot;&gt;Flask&lt;/a&gt; integrations? Give them a try for a superior monitoring experience with AppSignal!&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Flask or Django: Which One Best Fits Your Python Project?</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/06/25/flask-or-django-which-best-fits-your-python-project.html"/>
    <id>https://blog.appsignal.com/2025/06/25/flask-or-django-which-best-fits-your-python-project.html</id>
    <published>2025-06-25T00:00:00+00:00</published>
    <updated>2025-06-25T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s explore the key factors to take into account when deciding between Flask or Django for your Python app.</summary>
    <content type="html">&lt;p&gt;Choosing the right framework can significantly impact the success of a dev project.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;ve chosen to use Python for your project&amp;#39;s backend, you might need to decide between Flask and Django, the most popular web frameworks in the Python ecosystem.&lt;/p&gt;
&lt;p&gt;This article will help you determine which of the two best suits your specific needs by examining the key factors to consider.&lt;/p&gt;
&lt;p&gt;Before diving into the technicalities, let&amp;#39;s briefly introduce both Flask and Django.&lt;/p&gt;
&lt;h2&gt;Introducing Flask and Django for Python&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll begin by examining what Flask has to offer.&lt;/p&gt;
&lt;h2&gt;Introducing Flask&lt;/h2&gt;
&lt;p&gt;Flask is an open source, Python-based web microframework developed by Armin Ronacher in 2011. It comes with a lot of tools, technologies, and libraries. It offers form validation and other extensions for object-relational mapping, open authentication, file uploads, and more.&lt;/p&gt;
&lt;p&gt;Its key features and benefits are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lightweight and flexible&lt;/strong&gt;: As a microframework, Flask provides the essentials for web development without additional components by default. Its lightweight nature allows developers to pick and choose the libraries and tools that best fit their project requirements, providing maximum flexibility.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jinja2 templating&lt;/strong&gt;: Flask uses &lt;a href=&quot;https://palletsprojects.com/p/jinja/&quot;&gt;Jinja2&lt;/a&gt;, a powerful and flexible templating engine, for rendering dynamic HTML that supports template inheritance, macros, and filters. This streamlines the process of creating reusable and maintainable templates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extensions support&lt;/strong&gt;: Flask supports the introduction of third-party extensions to add functionality to web applications. Among the many extensions available are &lt;a href=&quot;https://github.com/pallets-eco/flask-sqlalchemy&quot;&gt;Flask-SQLAlchemy&lt;/a&gt;, which adds support for working with databases, and &lt;a href=&quot;https://github.com/maxcountryman/flask-login&quot;&gt;Flask-Login&lt;/a&gt; which provides user session management.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Development server and debugger&lt;/strong&gt;: Flask includes a built-in development server and debugger that provides error messages and interactive debugging tools. The debugger can execute code in the context of a stack trace, helping developers diagnose and fix issues efficiently during development.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Introducing Django&lt;/h2&gt;
&lt;p&gt;Django is an open source, Python-based web framework that was released in 2005 by Adrian Holovaty and Simon Willison. It provides full-stack development configurations, such as template layouts, requests and troubleshooting, cookies, form validation, unit tests, table settings, and other functionality used by developers to create dynamic web applications.&lt;/p&gt;
&lt;p&gt;Its offers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A comprehensive framework&lt;/strong&gt;: As a high-level framework, Django includes a wide range of built-in features like authentication, Object-Relational Mapping (ORM), and an admin interface. This &amp;quot;batteries-included&amp;quot; approach allows developers to focus on writing application-specific code rather than reinventing the wheel for common tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Object-Relational Mapping (ORM)&lt;/strong&gt;: Django’s ORM enables developers to interact with their database using Python code instead of SQL. This abstraction layer supports multiple database backends (including PostgreSQL, MySQL, and SQLite), simplifying database migrations and schema management.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin interface&lt;/strong&gt;: Django automatically generates an admin interface for defined models in an application. This feature is highly customizable and provides a quick way to manage and administer site content without additional coding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Django has robust built-in security features, such as protection against SQL injection, cross-site scripting (XSS), cross-site request forgery (CSRF), and clickjacking. It also encourages developers to adopt security best practices and provides mechanisms like password hashing and user authentication.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While Flask and Django are both Python-based and open source, the main difference between them is that Flask is a microframework, whereas Django is a full-featured framework. This is an important differentiator.&lt;/p&gt;
&lt;p&gt;Now, let&amp;#39;s dive into the key factors to consider when deciding which to choose for your web development project.&lt;/p&gt;
&lt;h2&gt;Comparing Flask and Django&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s make a head-to-head comparison between Flask and Django.&lt;/p&gt;
&lt;h3&gt;Learning Curve&lt;/h3&gt;
&lt;p&gt;An important factor to take into account is the learning curve needed for each framework.&lt;/p&gt;
&lt;p&gt;Flask is often celebrated for its simplicity and ease of use, making it an excellent choice for beginners and those who prefer a minimalistic approach.&lt;/p&gt;
&lt;p&gt;Flask is simple and easy to use because it is a micro-framework. This means that it provides the basic tools needed to build a web application and leaves developers to decide which additional components to use. Because it&amp;#39;s lightweight, developers can get started quickly.&lt;/p&gt;
&lt;p&gt;In contrast to Flask, Django follows a &amp;quot;batteries-included&amp;quot; philosophy, providing a comprehensive set of features. As we&amp;#39;ve mentioned, these include Object-Relational Mapping for database interactions, a powerful admin interface, authentication mechanisms, and a templating engine. While this richness of features can be overwhelming for beginners, it significantly reduces the amount of third-party code and configurations needed for more complex applications.&lt;/p&gt;
&lt;p&gt;To summarize, if you’re a beginner familiar with Python, it’s easy to get your head around Flask’s minimalist structure to start your project. Django is a more complex web solution that requires extensive expertise for sophisticated applications, so it is more suited for experts.&lt;/p&gt;
&lt;h3&gt;Development Time&lt;/h3&gt;
&lt;p&gt;Flask can significantly reduce development time, but you usually need to build most parts of your website from scratch, so it can be quite labor-intensive. If you&amp;#39;re an experienced developer, you might find your development time slowed down by Flask&amp;#39;s limited built-in features.&lt;/p&gt;
&lt;p&gt;On the other hand, Django supports rapid development, especially when you&amp;#39;re working under tight deadlines. This is because it comes with many built-in features that reduce the amount of code you need to write. For example, developers can create a minimum viable product (MVP) faster with Django than with Flask. This makes Django a great choice for starting large websites quickly.&lt;/p&gt;
&lt;p&gt;While Flask also allows for quick MVP development, Django has a clear advantage when it comes to the frontend. Django&amp;#39;s built-in template engine, in fact, speeds up development compared to Flask&amp;#39;s. Additionally, Django has many standard libraries that help developers build common functionalities and solve typical development problems.&lt;/p&gt;
&lt;h3&gt;Database Management&lt;/h3&gt;
&lt;p&gt;Django and Flask offer distinct approaches to database management.&lt;/p&gt;
&lt;p&gt;Django, as already mentioned, includes a robust ORM that supports a variety of relational databases such as SQLite, PostgreSQL, MySQL, and Oracle, making it an excellent choice for projects that utilize these databases. Its ORM simplifies the process of generating and managing database migrations and facilitates the creation of forms and views, which is ideal for web applications performing CRUD operations.&lt;/p&gt;
&lt;p&gt;However, consider that, for complex queries or projects using NoSQL databases, Django’s ORM can be limiting, although third-party projects like &lt;a href=&quot;https://django-mongodb-engine.readthedocs.io/en/latest/&quot;&gt;Django MongoDB Engine&lt;/a&gt; or &lt;a href=&quot;https://www.djongomapper.com/&quot;&gt;Djongo&lt;/a&gt; can provide some support.&lt;/p&gt;
&lt;p&gt;On the other hand, Flask provides developers with complete freedom in choosing how to handle data storage. This flexibility allows for the use of a wide range of libraries and extensions, such as the already mentioned Flask-SQLAlchemy for relational databases or &lt;a href=&quot;https://github.com/dcrosta/flask-pymongo&quot;&gt;Flask-PyMongo&lt;/a&gt; for MongoDB. While this flexibility can result in a steeper learning curve and a higher risk of errors, it also enables developers to select the ORM or ODM (Object Document Mapper) that best suits their specific needs.&lt;/p&gt;
&lt;p&gt;So, if your project relies heavily on a relational database, Django’s integrated ORM will likely make development smoother and more straightforward. Instead, if your project involves non-relational databases or requires custom data handling solutions, Flask’s flexibility might be more advantageous (despite the increased complexity and learning required).&lt;/p&gt;
&lt;h3&gt;Security&lt;/h3&gt;
&lt;p&gt;Flask includes some basic security features, like Cross-Site Request Forgery (CSRF) protection, but it relies heavily on third-party extensions for additional security measures. While Flask&amp;#39;s lower coding requirements can reduce the risk of certain cyber threats, the reliance on third-party extensions means that your application&amp;#39;s security depends on the strength of these plugins. This places a significant responsibility on your development team to regularly assess and update third-party libraries and extensions to ensure security remains robust.&lt;/p&gt;
&lt;p&gt;Django, on the other hand, comes with a wide range of built-in security features, including user password hashing, CSRF tokens, and authentication and authorization modules. These built-in tools help prevent common security mistakes and allow developers to run a comprehensive security checklist before deploying their applications. Additionally, the Django development team is proactive in identifying and reporting security vulnerabilities, ensuring security issues are addressed promptly.&lt;/p&gt;
&lt;p&gt;As a result, Django is generally easier to secure from the start, and to maintain throughout the lifecycle of your application.&lt;/p&gt;
&lt;h3&gt;Performance&lt;/h3&gt;
&lt;p&gt;When it comes to performance, Flask’s lightweight nature often translates to faster response times for simple applications. Its minimalistic design ensures there is little overhead, making it an excellent choice for high-performance applications where speed is important.&lt;/p&gt;
&lt;p&gt;You can consider the following general rules of thumb:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flask&lt;/strong&gt;: The response times are often faster for simple endpoints due to its minimalistic design and lower overhead. Flask&amp;#39;s lightweight nature means there’s less processing involved for each request compared to Django.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Django&lt;/strong&gt;: While Django can be slightly slower for very simple endpoints, it offers more built-in functionality and a robust framework for larger applications. The added features and middleware in Django, however, might introduce some additional latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, the actual response time differences between the two might be small, but Flask’s minimalism could lead to lower latency in scenarios where every millisecond counts. Django’s performance advantages come into play with more complex applications due to its powerful feature set and the optimizations available for larger projects.&lt;/p&gt;
&lt;h3&gt;Scalability&lt;/h3&gt;
&lt;p&gt;Both Flask and Django are capable of handling scalable applications, but they approach scalability differently.&lt;/p&gt;
&lt;p&gt;Flask’s micro-framework nature makes it easier to design a distributed architecture where components can be scaled independently. This can be particularly useful in microservice architectures, where different parts of an application are developed and deployed separately.&lt;/p&gt;
&lt;p&gt;Django’s scalability often comes from its integrated features that support large-scale applications. The framework includes tools for managing large databases, handling high traffic, and implementing advanced caching mechanisms. Django’s scalability is also enhanced by its compatibility with asynchronous task queues such as &lt;a href=&quot;https://docs.celeryq.dev/en/stable/&quot;&gt;Celery&lt;/a&gt;, allowing for efficient background processing.&lt;/p&gt;
&lt;h3&gt;Community and Ecosystem&lt;/h3&gt;
&lt;p&gt;In today&amp;#39;s tech world, a community around a product is a great asset for companies because it provides support as well as integration.&lt;/p&gt;
&lt;p&gt;Flask has a vibrant and active community, which is a significant advantage for developers. The community&amp;#39;s contributions have resulted in a rich ecosystem of third-party libraries and extensions that significantly extend Flask’s capabilities.&lt;/p&gt;
&lt;p&gt;Popular forums, GitHub repositories, and resources like Stack Overflow provide ample support for developers encountering issues or seeking advice.&lt;/p&gt;
&lt;p&gt;Here are some key resources for Flask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://flask.palletsprojects.com/en/3.0.x/&quot;&gt;Flask documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/humiaozuzu/awesome-flask&quot;&gt;Flask extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Community forums such as &lt;a href=&quot;https://stackoverflow.com/questions/tagged/flask&quot;&gt;Stack Overflow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Django’s community is one of its greatest strengths. The framework has been around since 2005, leading to a mature and well-established ecosystem. This longevity means that many common web development problems have already been solved, and solutions are readily available.&lt;/p&gt;
&lt;p&gt;The Django community is known for its robust third-party packages, which can be found in the Django Packages repository. Additionally, the &lt;a href=&quot;https://www.djangoproject.com/foundation/&quot;&gt;Django Software Foundation&lt;/a&gt; supports the development of the framework and organizes events like DjangoCon, fostering a strong sense of community and continuous improvement.&lt;/p&gt;
&lt;p&gt;Key resources for Django include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.djangoproject.com/en/5.0/&quot;&gt;Django documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://djangopackages.org/&quot;&gt;Django packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Community events such as &lt;a href=&quot;https://www.linkedin.com/company/djangocon-europe/&quot;&gt;DjangoCon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flask vs. Django Comparison Table&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s a table that summarizes all the key aspects of Flask and Django we&amp;#39;ve covered in this article, to help you make a better and more informed decision on which to choose:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Flask&lt;/th&gt;
&lt;th&gt;Django&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple and easy to use: ideal for beginners&lt;/td&gt;
&lt;td&gt;Rich feature set, steeper learning curve: suitable for experts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Development Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires building parts from scratch. Can be labor-intensive&lt;/td&gt;
&lt;td&gt;Rapid development with many built-in features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flexible, supports various ORMs and ODMs&lt;/td&gt;
&lt;td&gt;Integrated ORM, supports multiple relational databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic built-in security, relies on third-party extensions&lt;/td&gt;
&lt;td&gt;Highly secure by default with extensive built-in features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Faster for simple applications due to minimalistic design&lt;/td&gt;
&lt;td&gt;Slightly slower for simple endpoints, but optimized for larger applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Easy to design distributed architecture, good for microservices&lt;/td&gt;
&lt;td&gt;Integrated tools for large-scale applications and high traffic management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Community and Ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vibrant and active community, rich ecosystem of third-party libraries&lt;/td&gt;
&lt;td&gt;Mature and well-established community, robust third-party packages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Whether you choose Flask or Django depends on the specific needs of your project, your expertise as a developer (or your team&amp;#39;s capabilities), and your long-term goals.&lt;/p&gt;
&lt;p&gt;As we&amp;#39;ve covered, Flask’s simplicity and flexibility make it an excellent choice for smaller projects or applications requiring significant customization.&lt;/p&gt;
&lt;p&gt;Django, with its comprehensive feature set and integrated components, is ideal for complex projects that can benefit from built-in tools and a robust framework structure.&lt;/p&gt;
&lt;p&gt;By carefully evaluating your project&amp;#39;s requirements and considering the strengths and weaknesses of each framework, you can make an informed decision that will set the foundation for a robust Python project.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Ways to Optimize Your Code in Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/05/28/ways-to-optimize-your-code-in-python.html"/>
    <id>https://blog.appsignal.com/2025/05/28/ways-to-optimize-your-code-in-python.html</id>
    <published>2025-05-28T00:00:00+00:00</published>
    <updated>2025-05-28T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll dive into four ways to optimize your Python project and improve performance.</summary>
    <content type="html">&lt;p&gt;By optimizing Python code, you improve performance, reduce resource consumption, and enhance scalability. While Python is known for its simplicity and readability, these characteristics can sometimes come at the cost of efficiency.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll explore four ways to optimize your Python project and improve performance.&lt;/p&gt;
&lt;p&gt;First, we&amp;#39;ll look at how best to use data structures.&lt;/p&gt;
&lt;h2&gt;Efficient Use of Python Data Structures&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll use some of the most well-known Python data structures to optimize our code.&lt;/p&gt;
&lt;h3&gt;Lists Vs. Tuples&lt;/h3&gt;
&lt;p&gt;Lists and tuples are probably the most basic and well-known data structures in Python. They serve different purposes, so they have different performance characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lists are mutable, which means they can be modified after creation.&lt;/li&gt;
&lt;li&gt;Tuples, instead, are immutable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before diving deep into why there are performance differences, let&amp;#39;s write a code sample that creates a list and a tuple of 5 numbers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import timeit

# Calculate creation time
list_test = timeit.timeit(stmt=&amp;quot;[1, 2, 3, 4, 5]&amp;quot;, number=1000000)
tuple_test = timeit.timeit(stmt=&amp;quot;(1, 2, 3, 4, 5)&amp;quot;, number=1000000)

# Print results
print(f&amp;quot;List creation: {list_test: .3} seconds&amp;quot;)
print(f&amp;quot;Tuple creation: {tuple_test: .3} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;List creation:  0.135 seconds
Tuple creation:  0.0207 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To calculate performance differences, we use the &lt;code&gt;timeit&lt;/code&gt; module like so:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;stmt&lt;/code&gt; parameter defines the code snippet we want to evaluate. So, in the case of the &lt;code&gt;list_test&lt;/code&gt; variable, it evaluates a list of five numbers; in &lt;code&gt;tuple_test&lt;/code&gt;, it evaluates a tuple of five numbers.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;number&lt;/code&gt; parameter specifies how many times the &lt;code&gt;stmt&lt;/code&gt; parameter must be executed. In both cases, we run it 100,0000 times, meaning the code creates the list and the tuple 100,0000 times.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As the example shows, tuples are way faster than lists. Let&amp;#39;s dig into why:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory allocation&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Due to their immutability, tuples are stored in a fixed-size block of memory. The size of this block is determined when the tuple is created, and it doesn’t change. This fixed size makes tuple memory allocation fast.&lt;/li&gt;
&lt;li&gt;Lists, on the other hand, need to support dynamic resizing. This means they often allocate extra space to accommodate potential growth without requiring frequent reallocations.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Internal structure&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;The internal structure of a tuple consists essentially of a continuous block of memory with a fixed layout. This layout includes the elements themselves and some metadata (like size), but since tuples are immutable, the structure remains simple.&lt;/li&gt;
&lt;li&gt;Lists have a more complex internal structure to manage their mutability. They need to keep track of their current size and allocated capacity, and must handle changes in size dynamically.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caching and optimization&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Python can use various optimizations for tuples, such as caching, because their immutability guarantees that they won’t change after creation. These optimizations reduce the need for repeated memory allocation and speed up creation.&lt;/li&gt;
&lt;li&gt;While Python does optimize list operations, the potential for lists to change means that optimization is limited compared to tuples.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dictionaries and Sets Vs. Lists in Python&lt;/h3&gt;
&lt;p&gt;In Python, dictionaries and sets are data structures that allow for fast lookups. When you want to check if an item is in a set or find a value associated with a key in a dictionary, these operations typically take constant time; this is denoted as &lt;code&gt;O(1)&lt;/code&gt; in &lt;a href=&quot;https://www.freecodecamp.org/news/big-o-notation-why-it-matters-and-why-it-doesnt-1674cfa8a23c/&quot;&gt;&amp;quot;Big O notation&amp;quot;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Given their structure, using dictionaries and sets can significantly improve performance when you need to frequently check for the existence of an item or access elements by a key.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s show this with a code snippet. For example, suppose we create a dictionary, a set, and a list with 100,0000 numbers. We want to look for the number 999,999 and then work out how long it takes using these three different data structures:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time

# Create a large set, dictionary, and list
large_set = {i for i in range(1000000)}
large_dict = {i: str(i) for i in range(1000000)}
large_list = [i for i in range(1000000)]

# define element to lookup
element = 999999

# Timing set lookup
start_time = time.time()
found = element in large_set
end_time = time.time()
print(f&amp;quot;Set lookup took: {end_time - start_time:.8f} seconds&amp;quot;)

# Timing dictionary lookup
start_time = time.time()
found = element in large_dict
end_time = time.time()
print(f&amp;quot;Dictionary lookup took: {end_time - start_time:.8f} seconds&amp;quot;)

# Timing list lookup
start_time = time.time()
found = element in large_list
end_time = time.time()
print(f&amp;quot;List lookup took: {end_time - start_time:.8f} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Set lookup took: 0.00000000 seconds
Dictionary lookup took: 0.00000000 seconds
List lookup took: 0.00771618 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, basically, the time needed to search for the element 999,999 is (almost) 0 seconds for the set and the dictionary.&lt;/p&gt;
&lt;p&gt;Of course, if we want to compare the sets and the dictionary, we&amp;#39;ll find that the set provides better performance, as we may expect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import timeit

# Calculate timing performance for dictionary and set
dict_test = timeit.timeit(stmt=&amp;quot;&amp;#39;a&amp;#39; in {&amp;#39;a&amp;#39;: 1, &amp;#39;b&amp;#39;: 2, &amp;#39;c&amp;#39;: 3}&amp;quot;, number=1000000)
set_test = timeit.timeit(stmt=&amp;quot;1 in {1, 2, 3, 4, 5}&amp;quot;, number=1000000)

# Print results
print(f&amp;quot;Dictionary lookup: {dict_test: .3} seconds&amp;quot;)
print(f&amp;quot;Set lookup: {set_test: .3} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Dictionary lookup:  0.0821 seconds
Set lookup:  0.0212 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, how do dictionaries and sets achieve &lt;code&gt;O(1)&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Well, dictionaries and sets use a data structure called a &lt;a href=&quot;https://en.wikipedia.org/wiki/Hash_table#:~:text=In%20computing%2C%20a%20hash%20table,that%20maps%20keys%20to%20values.&quot;&gt;hash table&lt;/a&gt;. Here&amp;#39;s a simplified explanation of how it works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hashing&lt;/strong&gt;: When you add a key to a dictionary or an item to a set, Python computes a hash value (a fixed-size integer) from the key or item. This hash value determines where the data is stored in memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct access&lt;/strong&gt;: With the hash value, Python can directly access the location where the data is stored without searching the entire data structure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So it&amp;#39;s very fast to check if an item exists in a set or dictionary, which is useful for operations that require frequent existence checks.&lt;/p&gt;
&lt;h3&gt;Choosing the Right Data Structure&lt;/h3&gt;
&lt;p&gt;Choosing the appropriate data structure based on the specific needs of your application leads to significant performance gains.&lt;/p&gt;
&lt;p&gt;If you need to store data and you&amp;#39;re sure it won&amp;#39;t change over time, definitely use tuples to optimize your code.&lt;/p&gt;
&lt;p&gt;When you need to frequently look for elements, prefer sets and dictionaries over lists or tuples.&lt;/p&gt;
&lt;h2&gt;Global Variables, Encapsulation, and Namespace&lt;/h2&gt;
&lt;p&gt;In Python, scope determines the visibility and lifetime of a variable in a program. Variables can have different scopes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local Scope&lt;/strong&gt;: This refers to variables defined within a function. They are only accessible inside that function.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global Scope&lt;/strong&gt;: Variables defined at the top level of a script or module. They are accessible throughout the module.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Class/instance Scope&lt;/strong&gt;: Variables defined within a class, including class attributes and instance attributes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This section describes code optimization by avoiding global variables, using class encapsulation, and managing a namespace correctly.&lt;/p&gt;
&lt;h3&gt;Avoiding Global Variables&lt;/h3&gt;
&lt;p&gt;Local variables are faster to access compared to global variables, primarily due to the way Python manages variable scopes and lookups.&lt;/p&gt;
&lt;p&gt;In particular, Python uses the Local, Enclosing, Global, Built-in (LEGB) rule to resolve variable names:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local&lt;/strong&gt;: Names defined within a function.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enclosing&lt;/strong&gt;: Names in the local scopes of any enclosing functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global&lt;/strong&gt;: Names at the top level of the module or script.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in&lt;/strong&gt;: Preassigned names in the Python built-in namespace.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When accessing a variable, Python starts searching from the innermost scope (the local one). Since the local scope is limited to the function’s context, it contains fewer variables, making the search process quicker. On the contrary, global scope encompasses all top-level names in the module, resulting in a potentially larger search space.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s an example to show the difference in performance when using global vs. local variables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time

# Local variable test
def local_test():
    a = 0
    for var_1 in range(1000000):
        a += 1

# Global variable test
b = 0
def global_test():
    global b
    for var_2 in range(1000000):
        b += 1

start_time = time.time()
local_test()
local_time = time.time() - start_time
print(f&amp;quot;Local variable test:{local_time: .3} seconds&amp;quot;)

start_time = time.time()
global_test()
global_time = time.time() - start_time
print(f&amp;quot;Global variable test: {global_time: .3} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the result is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Local variable test: 0.0441 seconds
Global variable test:  0.0685 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, whenever possible, prefer using local variables to global ones.&lt;/p&gt;
&lt;h3&gt;Encapsulation&lt;/h3&gt;
&lt;p&gt;Encapsulating variables within functions and classes can improve performance by reducing the scope and limiting the number of variables the interpreter needs to track.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s see the difference in performance between using encapsulation and not using it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import timeit

# Class without encapsulation
class Rectangle:
    &amp;#39;&amp;#39;&amp;#39;This class creates a rectangle without encapsulation&amp;#39;&amp;#39;&amp;#39;
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

# Class with encapsulation
class EncapsulatedRectangle:
    &amp;#39;&amp;#39;&amp;#39;This class creates a rectangle with encapsulation&amp;#39;&amp;#39;&amp;#39;
    def __init__(self, width, height):
        self._width = width
        self._height = height

    def get_width(self):
        return self._width

    def set_width(self, width):
        self._width = width

    def get_height(self):
        return self._height

    def set_height(self, height):
        self._height = height

    def area(self):
        return self._width * self._height

    def perimeter(self):
        return 2 * (self._width + self._height)

# Create instances of both classes
rect = Rectangle(10, 20)
enc_rect = EncapsulatedRectangle(10, 20)

# Define the test functions
def test_rect_area():
    return rect.area()

def test_enc_rect_area():
    return enc_rect.area()

def test_rect_perimeter():
    return rect.perimeter()

def test_enc_rect_perimeter():
    return enc_rect.perimeter()

# Time the functions using timeit
iterations = 5000000

rect_area_time = timeit.timeit(test_rect_area, number=iterations)
enc_rect_area_time = timeit.timeit(test_enc_rect_area, number=iterations)
rect_perimeter_time = timeit.timeit(test_rect_perimeter, number=iterations)
enc_rect_perimeter_time = timeit.timeit(test_enc_rect_perimeter, number=iterations)

# Print results
print(f&amp;quot;Rectangle (no encapsulation) area time: {rect_area_time:.4f} seconds&amp;quot;)
print(f&amp;quot;Encapsulated Rectangle area time: {enc_rect_area_time:.4f} seconds&amp;quot;)
print(f&amp;quot;Rectangle (no encapsulation) perimeter time: {rect_perimeter_time:.4f} seconds&amp;quot;)
print(f&amp;quot;Encapsulated Rectangle perimeter time: {enc_rect_perimeter_time:.4f} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Rectangle (no encapsulation) area time: 2.1583 seconds
Encapsulated Rectangle area time: 2.2764 seconds
Rectangle (no encapsulation) perimeter time: 2.5185 seconds
Encapsulated Rectangle perimeter time: 2.4265 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Encapsulation can significantly improve performance in larger applications with numerous variables. By keeping variables local to functions and classes, you reduce the interpreter&amp;#39;s workload since it has fewer variables to manage. This leads to a faster execution time, especially in complex programs with many functions and classes.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;These are the key benefits of encapsulation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reduced scope&lt;/strong&gt;: By limiting the scope of variables to the smallest necessary context, the interpreter has fewer variables to track, which leads to faster execution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory management&lt;/strong&gt;: Local variables are automatically deallocated when a function exits, which helps with efficient memory use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoiding naming conflicts&lt;/strong&gt;: Encapsulation prevents variable name clashes, making code easily maintainable and less error-prone.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, you&amp;#39;d better use encapsulation to improve performance when creating classes. Also, note that encapsulation provides controlled access and modification of attributes, protecting data from outside modifications.&lt;/p&gt;
&lt;h3&gt;Correct Namespace Management&lt;/h3&gt;
&lt;p&gt;Minimizing global namespace pollution leads to better performance. The main idea, in this case, is to use modules and packages to organize your code and keep the global namespace clean. In other words, instead of using too many global variables and creating too many functions or classes, create modules and import them.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a general example of inefficient namespace management:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;global_var_1 = &amp;quot;...&amp;quot;

def first_function():
    &amp;#39;&amp;#39;&amp;#39;A function&amp;#39;&amp;#39;&amp;#39;
    ....

def second_function():
    &amp;#39;&amp;#39;&amp;#39;Another function&amp;#39;&amp;#39;&amp;#39;
    ...

def main_function():
    &amp;#39;&amp;#39;&amp;#39;The main function of the program&amp;#39;&amp;#39;&amp;#39;
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main idea is to change this to something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from functions.function_1 import *
from functions.function_2 import *

def main_function():
    &amp;#39;&amp;#39;&amp;#39;The main function of the program&amp;#39;&amp;#39;&amp;#39;
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To do so, you have to modularize your functions so that the structure of your folders becomes something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;main_folder/
|__ main.py
|
|__ functions/
        |__ function_1.py
        |__ function_2.py
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: In this case, &lt;code&gt;function_1.py&lt;/code&gt; and &lt;code&gt;function_2.py&lt;/code&gt; can be bigger than &lt;code&gt;first_function()&lt;/code&gt; and &lt;code&gt;second_function()&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, whenever possible, create modules and packages from your code. This helps with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: The code is more efficient on the machine side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Readability&lt;/strong&gt;: Shorter code is generally more easily readable than longer code. It&amp;#39;s better to have small connected programs than a big program.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reuse&lt;/strong&gt;: Any module or package you create can be used in other programs, helping you save time in the future.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Utilize List Comprehensions and Generator Expressions&lt;/h2&gt;
&lt;p&gt;This section describes how code performance can be improved through list comprehension and generators.&lt;/p&gt;
&lt;h3&gt;List Comprehension&lt;/h3&gt;
&lt;p&gt;List comprehension is a fast and concise way to create a new list using the power of loops and statements with one line of code.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s see the difference in performance first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import timeit

# Define the code snippets as functions
def loop_code():
    &amp;#39;&amp;#39;&amp;#39;This function creates a new list out of classic for loop&amp;#39;&amp;#39;&amp;#39;
    squares = []
    for x in range(10):
        squares.append(x**2)

def comprehension_code():
    &amp;#39;&amp;#39;&amp;#39;&amp;#39;This function created a new list out of a list comprehension&amp;#39;&amp;#39;&amp;#39;
    squares = [x**2 for x in range(10)]

# Measure execution time
loop_test = timeit.timeit(loop_code, number=1000000)
comprehension_test = timeit.timeit(comprehension_code, number=1000000)

print(f&amp;quot;Loop: {loop_test: .4} seconds&amp;quot;)
print(f&amp;quot;List comprehension: {comprehension_test: .4} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This leads to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Loop:  2.642 seconds
List comprehension:  2.444 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;List comprehension is more performance-friendly than standard for-loops because of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reduced overhead&lt;/strong&gt;: List comprehensions are implemented in C within the Python interpreter, making them faster (as lower-level optimizations aren&amp;#39;t accessible in a standard Python for-loop).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No method calls&lt;/strong&gt;: In a traditional for-loop, the &lt;code&gt;append()&lt;/code&gt; method is called repeatedly, which adds some overhead. List comprehensions avoid this by constructing the list in a single expression.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local scope&lt;/strong&gt;: Variables defined within a list comprehension are scoped more tightly than variables defined in a for-loop. This reduces the potential for variable conflicts and can sometimes make garbage collection more efficient.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, whenever possible, always prefer using list comprehension to create a new list. This enhances performance and code readability.&lt;/p&gt;
&lt;h3&gt;Generator Expressions&lt;/h3&gt;
&lt;p&gt;Generator expressions in Python provide a concise way to create generators without using a separate generator function with the &lt;code&gt;yield()&lt;/code&gt; method. They are similar to list comprehensions — the key difference being that they produce values one at a time and only when needed, which makes them more memory-efficient for large data sets.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s see how generator expressions can improve performance:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import timeit

# Define the size of the iterable
n = 1000000

# Generator expression
gen_expr = (i for i in range(n))

# List comprehension
list_comp = [i for i in range(n)]

# Measure the time taken by the generator expression
gen_time = timeit.timeit(&amp;#39;sum((i for i in range(n)))&amp;#39;, globals=globals(), number=10)

# Measure the time taken by the list comprehension
list_time = timeit.timeit(&amp;#39;sum([i for i in range(n)])&amp;#39;, globals=globals(), number=10)

print(f&amp;quot;Generator expression took {gen_time:.4f} seconds&amp;quot;)
print(f&amp;quot;List comprehension took {list_time:.4f} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Generator expression took 1.4823 seconds
List comprehension took 1.9738 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generator expressions are more efficient than list comprehension due to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lazy evaluation&lt;/strong&gt;: Generator expressions generate items on the fly. This means that they do not compute all items at once, which is memory efficient.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory efficiency&lt;/strong&gt;: Since values are produced one at a time, generator expressions use less memory compared to list comprehensions, especially for large datasets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you need to store values and use them only when needed, always prefer generators for good performance.&lt;/p&gt;
&lt;h2&gt;Leveraging Built-in Functions and Libraries&lt;/h2&gt;
&lt;p&gt;This section describes how optimizing code using built-in libraries and functions improves the performance of your machine.&lt;/p&gt;
&lt;h3&gt;Standard Library Efficiency&lt;/h3&gt;
&lt;p&gt;Python’s standard library functions are often implemented in C and optimized for speed. Using these functions leads to significant performance improvements, due to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lower-level operations&lt;/strong&gt;: C operates closer to the hardware level compared to Python, providing more efficient memory and CPU usage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimized algorithms&lt;/strong&gt;: Experienced developers highly optimize standard library functions to perform common tasks efficiently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduced overhead&lt;/strong&gt;: Invoking a function implemented in C avoids the overhead associated with Python&amp;#39;s dynamic typing and interpreted execution.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Suppose we sort a list with a lot of numbers. The following example compares performance when creating a custom function versus using a built-in one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time

# Sorting via custom function
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] &amp;gt; arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]

arr = [i for i in range(10000, 0, -1)]
start_time = time.time()

bubble_sort(arr)

end_time = time.time()
print(f&amp;quot;Bubble sort took: {end_time - start_time} seconds&amp;quot;)

# Sorting via built-in function
arr = [i for i in range(10000, 0, -1)]
start_time = time.time()

sorted(arr)

end_time = time.time()
print(f&amp;quot;Sorted function took: {end_time - start_time} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The performance results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Bubble sort took: 25.067534685134888 seconds
Sorted function took: 0.0 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The built-in &lt;code&gt;sorted()&lt;/code&gt; function immediately performs the sorting operation. The custom function, on the other hand, takes nearly 30 seconds to complete its tasks.&lt;/p&gt;
&lt;h3&gt;Using Third-party Libraries&lt;/h3&gt;
&lt;p&gt;We can also use third-party libraries like &lt;a href=&quot;https://numpy.org/&quot;&gt;NumPy&lt;/a&gt; (a library that brings the computational power of languages like C and Fortran to Python) and &lt;a href=&quot;https://pandas.pydata.org/&quot;&gt;Pandas&lt;/a&gt; (a fast, powerful, flexible, and easy-to-use open source data analysis and manipulation tool, built on top of the Python programming language) for performance optimizations. These libraries are highly optimized for numerical computations and data manipulation, so, for performance reasons, it&amp;#39;s always better to use them rather than to create a custom function.&lt;/p&gt;
&lt;p&gt;Suppose we want to add up an array&amp;#39;s elements. We can do so with a custom function or with the method &lt;code&gt;np.sum()&lt;/code&gt; by Numpy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
import numpy as np

def sum_array(arr):
    total = 0
    for num in arr:
        total += num
    return total

# Create a large array
large_array = list(range(1, 10000001))

# Measure time for custom function
start_time = time.time()
custom_sum = sum_array(large_array)
custom_duration = time.time() - start_time


# Convert the list to a NumPy array
large_array_np = np.array(large_array)

# Measure time for NumPy function
start_time = time.time()
numpy_sum = np.sum(large_array_np)
numpy_duration = time.time() - start_time

print(f&amp;quot;Duration with custom function: {custom_duration: .4} seconds&amp;quot;)
print(f&amp;quot;Duration with Numpy: {numpy_duration: .4} seconds&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&amp;#39;s the result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Duration with custom function:  1.023 seconds
Duration with Numpy:  0.008745 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difference in performance is huge in this case!&lt;/p&gt;
&lt;p&gt;So, remember: you don&amp;#39;t need to reinvent the wheel. One of Python&amp;#39;s superpowers is that it relies on a vast range of both standard and third-party libraries. You can always use them to save coding and computation time.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we&amp;#39;ve described four ways to optimize your Python code to improve your machine&amp;#39;s performance (and save coding time). We hope you find these tips and tricks useful.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Using JWTs in Python Flask REST Framework</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/04/30/using-jwts-in-python-flask-rest-framework.html"/>
    <id>https://blog.appsignal.com/2025/04/30/using-jwts-in-python-flask-rest-framework.html</id>
    <published>2025-04-30T00:00:00+00:00</published>
    <updated>2025-04-30T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll build a JWT-based authentication system by creating a to-do list API using Flask.</summary>
    <content type="html">&lt;p&gt;JSON Web Tokens (JWTs) secure communication between parties over the internet by authenticating users and transmitting information securely, without requiring a centralized storage system.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll explain what JWTs are and give a high-level overview of how they work. We&amp;#39;ll also implement a JWT-based authentication system by creating a to-do list API using Flask.&lt;/p&gt;
&lt;p&gt;So, whether you&amp;#39;re a beginner or an experienced Python developer, this guide aims to enhance your understanding of JWTs and their practical application in Flask.&lt;/p&gt;
&lt;p&gt;First, let&amp;#39;s provide an overview of JWTs and explain how they work, at a high level.&lt;/p&gt;
&lt;h2&gt;JWTs: How They Work&lt;/h2&gt;
&lt;p&gt;JWTs are a compact and self-contained method for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed using a secret or a public/private key pair.&lt;/p&gt;
&lt;p&gt;A JWT is composed of three parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Header&lt;/strong&gt;: Contains metadata about the token, including the type of token and the hashing algorithm used to sign it. Typical cryptographic algorithms include HMAC with SHA-256 (HS256) and an RSA signature with SHA-256 (RS256). Here&amp;#39;s an example of how this might look:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;alg&amp;quot;: &amp;quot;HS256&amp;quot;,
  &amp;quot;typ&amp;quot;: &amp;quot;JWT&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, &lt;code&gt;alg&lt;/code&gt; specifies the hashing algorithm used, while &lt;code&gt;typ&lt;/code&gt; indicates the token type (which, of course, is &amp;quot;JWT&amp;quot;). &lt;a href=&quot;https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields&quot;&gt;Read about other commonly used header fields&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Payload&lt;/strong&gt;: Contains the claims, which are statements about an entity (typically, the user) and additional data that can include information like the user ID, username, and roles. Here&amp;#39;s an example of how the payload might look:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;sub&amp;quot;: &amp;quot;1234567890&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;John Doe&amp;quot;,
  &amp;quot;admin&amp;quot;: true,
  &amp;quot;iat&amp;quot;: 1516239022
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, &lt;code&gt;sub&lt;/code&gt; is the subject identifier and represents the subject of the token (e.g., the user ID); &lt;code&gt;name&lt;/code&gt; is the name of the user; &lt;code&gt;admin&lt;/code&gt; is a boolean indicating administrative privileges; &lt;code&gt;iat&lt;/code&gt; means &amp;quot;issued at time&amp;quot; and represents when the token was issued (in the Unix timestamp).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Signature&lt;/strong&gt;: The signature is used to verify the authenticity of the token and to ensure that it hasn&amp;#39;t been tampered with. It is created by encoding the header and payload, concatenating them with a period, and then hashing them using the algorithm specified in the header along with a secret key.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, when putting everything together, the structure of a JWT looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;{Header in Base64URL}.{Payload in Base64URL}.{Signature}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When decoded, the JWT reveals the original JSON structures:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;header&amp;quot;: {
    &amp;quot;alg&amp;quot;: &amp;quot;HS256&amp;quot;,
    &amp;quot;typ&amp;quot;: &amp;quot;JWT&amp;quot;
  },
  &amp;quot;payload&amp;quot;: {
    &amp;quot;sub&amp;quot;: &amp;quot;1234567890&amp;quot;,
    &amp;quot;name&amp;quot;: &amp;quot;John Doe&amp;quot;,
    &amp;quot;admin&amp;quot;: true,
    &amp;quot;iat&amp;quot;: 1516239022
  },
  &amp;quot;signature&amp;quot;: &amp;quot;SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why JWTs Are Useful for Secure User Authentication in APIs&lt;/h3&gt;
&lt;p&gt;JWTs offer several benefits when securing user authentication in APIs, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stateless sessions&lt;/strong&gt;: JWTs eliminate the need to store session information on the server, as all the necessary data is contained within a token. This makes scaling your application easier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Because JWTs are signed, the server can verify the integrity and authenticity of the token. This ensures that the data hasn&amp;#39;t been altered and comes from a trusted source. This also helps protect endpoints by ensuring that only authenticated users can access certain resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: JWTs are compact, making them ideal for inclusion in HTTP headers and efficient for network transmission.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: They can include a variety of claims, allowing you to convey user roles, permissions, and other metadata necessary for authorization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-domain support&lt;/strong&gt;: JWTs can be used across different domains without issues, making them suitable for Single Sign-On (SSO) systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplified authentication flow&lt;/strong&gt;: They streamline the authentication process, reducing the complexity of managing user sessions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up the Flask Environment for a Flask Python App&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s now dive into a practical example to show how to implement theory into practice.&lt;/p&gt;
&lt;p&gt;But before that, let&amp;#39;s first set up a Flask environment with all the necessary dependencies.&lt;/p&gt;
&lt;h3&gt;Installing Dependencies and Project Structure&lt;/h3&gt;
&lt;p&gt;First, make sure you have Python 3.6+ installed. Then, create a project directory like this one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;jwt_flask_todo/
├── app.py
└── venv/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app.py&lt;/code&gt; is the main Flask application file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;venv/&lt;/code&gt; is the virtual environment where dependencies will be installed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To create the virtual environment, navigate to the root folder of your project and execute the command below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m venv venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then activate it:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux/MacOS:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source ./venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;.\venv\Scripts\activate.bat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can now install the libraries needed to replicate the Flask app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install Flask Flask-JWT-Extended
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;Flask-JWT-Extended&lt;/code&gt; is the library used in the following code examples to manage JWT tokens.&lt;/p&gt;
&lt;h2&gt;Implementing JWT Authentication in a Flask API&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll start our to-do list API by implementing user registration and login functionality, integrating JWTs for authentication.&lt;/p&gt;
&lt;h3&gt;Building a User Registration System&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s first create an endpoint to register users, storing their data in an in-memory dictionary for simplicity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Flask, request, jsonify

app = Flask(__name__)

# In-memory &amp;#39;database&amp;#39; of users
users = {}

@app.route(&amp;#39;/register&amp;#39;, methods=[&amp;#39;POST&amp;#39;])
def register():
    username = request.json.get(&amp;#39;username&amp;#39;)
    password = request.json.get(&amp;#39;password&amp;#39;)

    # Input validation
    if not username or not password:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Missing username or password&amp;quot;}), 400

    # Check if user already exists
    if username in users:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;User already exists&amp;quot;}), 400

    # Store user
    users[username] = password
    return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;User registered successfully&amp;quot;}), 201

if __name__ == &amp;#39;__main__&amp;#39;:
    app.run(debug=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This endpoint does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data retrieval&lt;/strong&gt;: Extracts a username and password from the JSON request body.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation&lt;/strong&gt;: Checks if the username and password are provided.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User existence check&lt;/strong&gt;: Ensures the username isn&amp;#39;t already taken.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User storage&lt;/strong&gt;: Adds the new user to the users dictionary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response&lt;/strong&gt;: Returns a success message if everything works as expected.&lt;/li&gt;
&lt;/ul&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h3&gt;Adding User Login with JWTs&lt;/h3&gt;
&lt;p&gt;Next, let&amp;#39;s create a login endpoint that issues a JWT token when a user logs in successfully:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_jwt_extended import JWTManager, create_access_token

# Configure the JWT secret key
app.config[&amp;#39;JWT_SECRET_KEY&amp;#39;] = &amp;#39;your_jwt_secret_key&amp;#39;
jwt = JWTManager(app)

@app.route(&amp;#39;/login&amp;#39;, methods=[&amp;#39;POST&amp;#39;])
def login():
    username = request.json.get(&amp;#39;username&amp;#39;)
    password = request.json.get(&amp;#39;password&amp;#39;)

    # Input validation
    if not username or not password:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Missing username or password&amp;quot;}), 400

    # Authentication
    if users.get(username) != password:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Invalid credentials&amp;quot;}), 401

    # Create JWT
    access_token = create_access_token(identity=username)
    return jsonify(access_token=access_token), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This endpoint is similar to the user registration one. In addition, it does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: Checks if the provided password matches the stored one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token creation&lt;/strong&gt;: Generates an access token with the &lt;code&gt;create_access_token&lt;/code&gt; method, embedding the username as the identity.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Protecting To-Do List API Endpoints with JWTs&lt;/h3&gt;
&lt;p&gt;Now that you have implemented a user registration and login, you can secure the endpoints so that only authenticated users can access them.&lt;/p&gt;
&lt;p&gt;Apply the &lt;code&gt;@jwt_required()&lt;/code&gt; decorator to the routes you want to protect. Use &lt;code&gt;get_jwt_identity()&lt;/code&gt; to retrieve the current user&amp;#39;s identity from the token:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_jwt_extended import JWTManager, jwt_required, get_jwt_identity

# Initialize the JWTManager:
app = Flask(__name__)
app.config[&amp;#39;JWT_SECRET_KEY&amp;#39;] = &amp;#39;your_jwt_secret_key&amp;#39;
jwt = JWTManager(app)

# Secure the endpoint
@app.route(&amp;#39;/protected&amp;#39;, methods=[&amp;#39;GET&amp;#39;])
@jwt_required()
def protected():
    # Access the identity of the current user w
    current_user = get_jwt_identity()
    return jsonify(logged_in_as=current_user), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, here&amp;#39;s what this code does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@jwt_required()&lt;/code&gt; decorator&lt;/strong&gt;: Ensures that the endpoint can only be accessed if a valid JWT is present in the request. If the JWT is missing or invalid, the request will be rejected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;get_jwt_identity()&lt;/code&gt; function&lt;/strong&gt;: Inside the protected route, this function retrieves a current user&amp;#39;s identity from the JWT. The identity is the value you set when creating the token (usually, the username or user ID).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Creating a To-Do List API&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s now build a simple to-do list API where each user can manage their tasks. We&amp;#39;ll create endpoints to manage the classical CRUD operations we&amp;#39;d expect to see in such an app.&lt;/p&gt;
&lt;h3&gt;Creating a Task&lt;/h3&gt;
&lt;p&gt;First, let&amp;#39;s create a task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;#39;/tasks&amp;#39;, methods=[&amp;#39;POST&amp;#39;])
@jwt_required()
def add_task():
    current_user = get_jwt_identity()
    task = request.json.get(&amp;#39;task&amp;#39;)

    if not task:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task content missing&amp;quot;}), 400

    # Initialize user&amp;#39;s task list if it doesn&amp;#39;t exist
    if current_user not in to_do_lists:
        to_do_lists[current_user] = []

    # Add task to user&amp;#39;s task list
    to_do_lists[current_user].append(task)
    return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task added&amp;quot;, &amp;quot;task&amp;quot;: task}), 201
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This endpoint does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@jwt_required()&lt;/code&gt;&lt;/strong&gt;: Ensures that only authenticated users can access the endpoint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;get_jwt_identity()&lt;/code&gt;&lt;/strong&gt;: Retrieves the current user&amp;#39;s username from the JWT.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task retrieval&lt;/strong&gt;: Gets the task content from the request JSON.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation&lt;/strong&gt;: Checks if the task content is provided.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task list initialization&lt;/strong&gt;: Creates an empty list for the user if they don&amp;#39;t have one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adding task&lt;/strong&gt;: Appends the task to the user&amp;#39;s list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response&lt;/strong&gt;: Returns a confirmation message with the task added.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Viewing All Tasks&lt;/h3&gt;
&lt;p&gt;This endpoint fetches the task list for a current user and returns an empty list if none exists:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;#39;/tasks&amp;#39;, methods=[&amp;#39;GET&amp;#39;])
@jwt_required()
def get_tasks():
    current_user = get_jwt_identity()
    tasks = to_do_lists.get(current_user, [])
    return jsonify({&amp;quot;tasks&amp;quot;: tasks}), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Updating a Task&lt;/h3&gt;
&lt;p&gt;Now we&amp;#39;ll update a task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;#39;/tasks/&amp;lt;int:task_id&amp;gt;&amp;#39;, methods=[&amp;#39;PUT&amp;#39;])
@jwt_required()
def update_task(task_id):
    current_user = get_jwt_identity()
    tasks = to_do_lists.get(current_user, [])

    if task_id &amp;lt; 0 or task_id &amp;gt;= len(tasks):
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task not found&amp;quot;}), 404

    updated_task = request.json.get(&amp;#39;task&amp;#39;)
    if not updated_task:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task content missing&amp;quot;}), 400

    tasks[task_id] = updated_task
    return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task updated&amp;quot;, &amp;quot;task&amp;quot;: updated_task}), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this endpoint:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;task_id&lt;/code&gt; parameter&lt;/strong&gt;: Represents the index of the task in the user&amp;#39;s task list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index validation&lt;/strong&gt;: Ensures the task ID is within valid bounds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Updated task content&lt;/strong&gt;: Retrieves the new task content from the request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Updating task&lt;/strong&gt;: Replaces the existing task with the updated content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response&lt;/strong&gt;: Confirms the update with the new task.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Deleting a Task&lt;/h3&gt;
&lt;p&gt;Finally, this endpoint removes the task at the specified index:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;#39;/tasks/&amp;lt;int:task_id&amp;gt;&amp;#39;, methods=[&amp;#39;DELETE&amp;#39;])
@jwt_required()
def delete_task(task_id):
    current_user = get_jwt_identity()
    tasks = to_do_lists.get(current_user, [])

    if task_id &amp;lt; 0 or task_id &amp;gt;= len(tasks):
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task not found&amp;quot;}), 404

    deleted_task = tasks.pop(task_id)
    return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task deleted&amp;quot;, &amp;quot;task&amp;quot;: deleted_task}), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For longer-lived sessions, you can allow users to refresh their tokens instead of having to log in again after every expiration.&lt;/p&gt;
&lt;h2&gt;Implementing Token Refresh for Longer Sessions&lt;/h2&gt;
&lt;p&gt;You can set up token refreshes for longer sessions in a few different ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update the JWT configuration.&lt;/li&gt;
&lt;li&gt;Modify the login endpoint to issue refresh tokens.&lt;/li&gt;
&lt;li&gt;Create a new endpoint that manages token refreshes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;#39;s take each approach in turn.&lt;/p&gt;
&lt;h3&gt;Updating JWT Configuration&lt;/h3&gt;
&lt;p&gt;JWTs expiration time can be managed like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from datetime import timedelta

app.config[&amp;#39;JWT_ACCESS_TOKEN_EXPIRES&amp;#39;] = timedelta(minutes=15)  # Access tokens expire after 15 minutes
app.config[&amp;#39;JWT_REFRESH_TOKEN_EXPIRES&amp;#39;] = timedelta(days=30)    # Refresh tokens expire after 30 days
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Modifying the Login Endpoint to Issue Refresh Tokens&lt;/h3&gt;
&lt;p&gt;To use both access and refresh tokens, you can update the login endpoint as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_jwt_extended import create_refresh_token

@app.route(&amp;#39;/login&amp;#39;, methods=[&amp;#39;POST&amp;#39;])
def login():
    # ... [existing code]

    # Authentication
    if users.get(username) != password:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Invalid credentials&amp;quot;}), 401

    # Create tokens
    access_token = create_access_token(identity=username)
    refresh_token = create_refresh_token(identity=username)
    return jsonify(access_token=access_token, refresh_token=refresh_token), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, you can use the &lt;code&gt;create_refresh_token()&lt;/code&gt; method to generate a refresh token for the user and prolong their session.&lt;/p&gt;
&lt;h3&gt;Adding Token Refresh Endpoint&lt;/h3&gt;
&lt;p&gt;As in the last case, you can create an endpoint to refresh access tokens using the refresh token:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_jwt_extended import jwt_refresh_token_required

@app.route(&amp;#39;/refresh&amp;#39;, methods=[&amp;#39;POST&amp;#39;])
@jwt_refresh_token_required
def refresh():
    current_user = get_jwt_identity()
    new_access_token = create_access_token(identity=current_user)
    return jsonify(access_token=new_access_token), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, though, the &lt;code&gt;@jwt_refresh_token_required&lt;/code&gt; decorator ensures the endpoint is accessed with a valid refresh token. Then, the &lt;code&gt;create_access_token()&lt;/code&gt; method creates a new access token to refresh the endpoint.&lt;/p&gt;
&lt;h2&gt;Logging Out and Managing User Sessions&lt;/h2&gt;
&lt;p&gt;Since JWTs are stateless, they cannot be invalidated server-side once issued. For this reason, you can implement token blocklisting to simulate a logout.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s see how.&lt;/p&gt;
&lt;h3&gt;Blocklisting Tokens&lt;/h3&gt;
&lt;p&gt;Revoked tokens can be maintained into a blocklist like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_jwt_extended import get_raw_jwt

# Set to store blocklisted tokens
blocklisted_tokens = set()

@app.route(&amp;#39;/logout&amp;#39;, methods=[&amp;#39;DELETE&amp;#39;])
@jwt_required()
def logout():
    jti = get_raw_jwt()[&amp;#39;jti&amp;#39;]
    blocklisted_tokens.add(jti)
    return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Successfully logged out&amp;quot;}), 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;get_raw_jwt()&lt;/code&gt; method retrieves the JWT data, including the &lt;code&gt;jti&lt;/code&gt; (which is the JWT ID). Then, this endpoint adds the token&amp;#39;s &lt;code&gt;jti&lt;/code&gt; to the &lt;code&gt;blocklisted_tokens&lt;/code&gt; set, simulating a logout. Once a token is inserted into the blocklist, any attempt to use it to log in will fail.&lt;/p&gt;
&lt;p&gt;So, before processing any request, you could check if the token a user is trying to use has been revoked. This could be done like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_jwt_extended import jwt_required, get_jwt_claims

@app.before_request
def check_revoked_token():
    jti = get_raw_jwt().get(&amp;#39;jti&amp;#39;)
    if jti in blocklisted_tokens:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Token revoked&amp;quot;}), 401
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, this piece of code can run before every request and checks if the token&amp;#39;s &lt;code&gt;jti&lt;/code&gt; is in the blocklist. If it is, it returns a response indicating that the token has been revoked.&lt;/p&gt;
&lt;h2&gt;Adding Permissions and Differentiating Between User Roles&lt;/h2&gt;
&lt;p&gt;To enhance security, you can manage control access to certain endpoints. This can be achieved, for example, through Role-Based Access Control (RBAC).&lt;/p&gt;
&lt;p&gt;So, let&amp;#39;s see how you can implement a simple RBAC system that differentiates between standard users and admins. But before that, bear in mind that in this article:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Standard users&lt;/strong&gt; can only manage their own tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin users&lt;/strong&gt; have full access, including the ability to delete any task.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Modifying User Registration to Include Roles&lt;/h3&gt;
&lt;p&gt;First, you have to modify a registration endpoint to allow setting a user role. This can be done like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;#39;/register&amp;#39;, methods=[&amp;#39;POST&amp;#39;])
def register():
    # Extract data from the request
    username = request.json.get(&amp;#39;username&amp;#39;)
    password = request.json.get(&amp;#39;password&amp;#39;)
    role = request.json.get(&amp;#39;role&amp;#39;, &amp;#39;user&amp;#39;)  # Default role is &amp;#39;user&amp;#39;

    # Input validation
    if not username or not password:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Missing username or password&amp;quot;}), 400

    # Check if user already exists
    if username in users:
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;User already exists&amp;quot;}), 400

    # Store user with role
    users[username] = {&amp;#39;password&amp;#39;: password, &amp;#39;role&amp;#39;: role}
    return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;User registered successfully&amp;quot;}), 201
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s what&amp;#39;s happening in this endpoint now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Role assignment&lt;/strong&gt;: Now there&amp;#39;s the possibility of setting a role during registration. In particular, note that the default assigned value is &lt;code&gt;user&lt;/code&gt; if no role is explicitly provided.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User data structure&lt;/strong&gt;: The code stores user information as a dictionary containing the password and role. In other words, it stores the user with its own role.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Protecting Endpoints Based on Roles&lt;/h3&gt;
&lt;p&gt;To improve endpoint protection even further, you can protect based on users&amp;#39; roles.&lt;/p&gt;
&lt;p&gt;Create a custom decorator that checks a user&amp;#39;s role before allowing access to certain endpoints, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from functools import wraps
from flask import abort

def role_required(required_role):
    def decorator(f):
        @wraps(f)
        @jwt_required()
        def wrapper(*args, **kwargs):
            claims = get_jwt()
            user_role = claims.get(&amp;#39;role&amp;#39;, &amp;#39;user&amp;#39;)
            if user_role != required_role:
                abort(403, description=&amp;quot;Forbidden: Insufficient privileges&amp;quot;)
            return f(*args, **kwargs)
        return wrapper
    return decorator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, the &lt;code&gt;role_required&lt;/code&gt; decorator checks if the user&amp;#39;s role matches the &lt;code&gt;required_role&lt;/code&gt; (the one needed to get access). If the user&amp;#39;s role doesn&amp;#39;t match, the endpoint aborts with a &lt;code&gt;403 Forbidden error&lt;/code&gt; message, meaning that the requester doesn&amp;#39;t have the right privileges to authenticate the endpoint.&lt;/p&gt;
&lt;p&gt;Finally, we&amp;#39;ll take a brief look at setting permissions so that only admins can delete tasks.&lt;/p&gt;
&lt;h3&gt;Specific Operations: Only Admins Can Delete Tasks&lt;/h3&gt;
&lt;p&gt;There are cases where deleting something should only be allowed for administrators.&lt;/p&gt;
&lt;p&gt;This can be implemented in the to-do list app we created by modifying the delete task endpoint, allowing only admins to delete tasks. Here&amp;#39;s how:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_jwt_extended import get_jwt

@app.route(&amp;#39;/tasks/&amp;lt;int:task_id&amp;gt;&amp;#39;, methods=[&amp;#39;DELETE&amp;#39;])
@role_required(&amp;#39;admin&amp;#39;)
def delete_task(task_id):
    # Get all tasks (for all users)
    all_tasks = [task for tasks in to_do_lists.values() for task in tasks]

    # Check if task exists
    if task_id &amp;lt; 0 or task_id &amp;gt;= len(all_tasks):
        return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task not found&amp;quot;}), 404

    # Find and delete the task
    for user_tasks in to_do_lists.values():
        if task_id &amp;lt; len(user_tasks):
            deleted_task = user_tasks.pop(task_id)
            return jsonify({&amp;quot;msg&amp;quot;: &amp;quot;Task deleted by admin&amp;quot;, &amp;quot;task&amp;quot;: deleted_task}), 200
        else:
            task_id -= len(user_tasks)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now, the delete task endpoint uses the &lt;code&gt;@role_required(&amp;#39;admin&amp;#39;)&lt;/code&gt; decorator to ensure only admins can access this endpoint. This means that only admins are allowed to delete any task from any user. Then, it confirms that the deletion is done.&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s it!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;By creating a to-do list API, we demonstrated how JWTs can secure user data and ensure that only authenticated users can access or modify their tasks.&lt;/p&gt;
&lt;p&gt;We covered registration, login, task management, token refresh, and logout functionality, giving you the foundation to expand these concepts further!&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Instrumenting Redis in Python with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/04/08/instrumenting-redis-in-python-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2025/04/08/instrumenting-redis-in-python-with-appsignal.html</id>
    <published>2025-04-08T00:00:00+00:00</published>
    <updated>2025-04-08T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s instrument Redis in a Python application using AppSignal.</summary>
    <content type="html">&lt;p&gt;Using AppSignal, you can monitor the performance of your Redis calls and get insights into how your application uses Redis. The AppSignal Python agent supports instrumenting Redis calls out of the box, so you can start monitoring your usage with just a few lines of code.&lt;/p&gt;
&lt;p&gt;In this post, we will cover how to instrument Redis in a Python application using AppSignal. We will build a simple URL shortener that stores data in a local Redis instance and uses AppSignal to monitor performance. Latencies, errors, and other metrics will be available in your dashboard in AppSignal, so you can keep an eye on how your application uses Redis.&lt;/p&gt;
&lt;p&gt;Ready? Let’s get started!&lt;/p&gt;
&lt;h2&gt;Spin Up a Python Project&lt;/h2&gt;
&lt;p&gt;We will assume you have a Redis instance running locally and a basic understanding of Python. You can &lt;a href=&quot;https://redis.io/download&quot;&gt;follow the instructions on the Redis website to install Redis&lt;/a&gt; on your machine. If you are using Windows, you can use the Windows Subsystem for Linux (WSL) to run Redis.&lt;/p&gt;
&lt;p&gt;First, let’s check that the Redis service is on your machine by running the following command in your terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;gt; redis-cli ping
PONG
&amp;gt; lsof -i :6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;PONG&lt;/code&gt; is the response and the &lt;code&gt;lsof&lt;/code&gt; command doesn&amp;#39;t output anything, then Redis is running on your machine.&lt;/p&gt;
&lt;p&gt;Next, let’s create a new Python project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;gt; mkdir url-shortener
&amp;gt; cd url-shortener
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let’s build the &lt;code&gt;requirements.txt&lt;/code&gt; file that will contain the dependencies we need for the project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;appsignal
opentelemetry-instrumentation-redis
opentelemetry-instrumentation-flask
redis
pyshorteners
Flask
flask-restful
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s an explanation of each dependency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;appsignal&lt;/code&gt;: The AppSignal Python agent that helps us monitor our application&amp;#39;s performance.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opentelemetry-instrumentation-redis&lt;/code&gt;: The OpenTelemetry instrumentation for Redis to instrument our Redis calls.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opentelemetry-instrumentation-flask&lt;/code&gt;: The OpenTelemetry instrumentation for Flask to instrument our Flask application.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redis&lt;/code&gt;: The supported Redis client for Python that we will use to interact with Redis.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pyshorteners&lt;/code&gt;: A library that helps us shorten URLs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Flask&lt;/code&gt;: The Flask web framework that we will use to build the URL shortener.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flask-restful&lt;/code&gt;: A library that helps us build RESTful APIs with Flask.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: Keep in mind that the AppSignal Python agent does not work on Windows. If you are using Windows, you can use the Windows Subsystem for Linux (WSL) to run the AppSignal agent.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, let’s install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;gt; pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The AppSignal agent needs an API key to connect to the AppSignal service. You can get your API key by &lt;a href=&quot;https://appsignal.com/users/sign_up&quot;&gt;signing up for an AppSignal account&lt;/a&gt; — there&amp;#39;s a free trial available.&lt;/p&gt;
&lt;p&gt;Once you have an account, you can configure the AppSignal agent via this command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;gt; python -m appsignal install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A series of prompts will ask for your API key and application name. You can use the default values or provide your own. This install command will create an &lt;code&gt;__appsignal__.py&lt;/code&gt; file in your project with the configuration for the AppSignal agent.&lt;/p&gt;
&lt;p&gt;You can now inspect the &lt;code&gt;__appsignal__.py&lt;/code&gt; file that was created in your project.&lt;/p&gt;
&lt;p&gt;Alternatively, you can create the &lt;code&gt;__appsignal__.py&lt;/code&gt; file manually with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# __appsignal__.py
from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name=&amp;quot;url-shortener&amp;quot;,
    push_api_key=&amp;quot;YOUR_API_KEY&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All set! Now, &lt;a href=&quot;https://www.appsignal.com/python/redis-monitoring&quot;&gt;let’s instrument Redis in our application&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Instrumenting Redis&lt;/h2&gt;
&lt;p&gt;With the AppSignal agent installed and configured, we can now instrument our application.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;shorten-url.py&lt;/code&gt; file with the following instructions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# shorten-url.py
import appsignal
from openTelemetry.instrumentation.redis import RedisInstrumentor

# open telemetry instrumentation
appsignal.start()
RedisInstrumentor().instrument()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;appsignal.start()&lt;/code&gt; function will start the AppSignal agent and configure it with the settings from the &lt;code&gt;__appsignal__.py&lt;/code&gt; file. Be sure to put these lines at the top of your Python file to ensure the AppSignal agent starts before any other code is executed.&lt;/p&gt;
&lt;p&gt;Once the AppSignal agent starts, we can use the &lt;code&gt;RedisInstrumentor&lt;/code&gt; class from the &lt;code&gt;opentelemetry-instrumentation-redis&lt;/code&gt; package to instrument our Redis calls. The &lt;code&gt;instrument()&lt;/code&gt; method will configure the Redis client and start collecting metrics.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you do not see any metrics in the AppSignal dashboard, it is likely because either the AppSignal agent is not running or the Redis instrumentation is not working.&lt;/em&gt;&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;To troubleshoot the instrumentation, you can use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;gt; python -m appsignal diagnose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will check the configuration of the AppSignal agent and the instrumentation of the Redis client. If there are any issues, the command will output an error message that you can use to fix the problem.&lt;/p&gt;
&lt;p&gt;Alternatively, you can submit your report to the AppSignal support team for help. The report will be made available via a web link you can share with the support team.&lt;/p&gt;
&lt;p&gt;Next, we will build an app that shortens URLs using this instrumentation.&lt;/p&gt;
&lt;h2&gt;Building the URL Shortener&lt;/h2&gt;
&lt;p&gt;We will use the &lt;code&gt;pyshorteners&lt;/code&gt; library to shorten URLs and the &lt;code&gt;redis&lt;/code&gt; library to store the data in Redis. In the &lt;code&gt;shorten-url.py&lt;/code&gt; file, simply add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# shorten-url.py
import appsignal
from openTelemetry.instrumentation.redis import RedisInstrumentor

# open telemetry instrumentation
appsignal.start()
RedisInstrumentor().instrument()

import redis
from flask import Flask, request
from flask_restful import Resource, Api

from urllib.parse import urlparse
import pyshorteners

app = Flask(__name__)
api = Api(app)


class ApiResource(Resource):
    def post(self):
        data = request.get_json()[&amp;#39;url&amp;#39;]

        def is_valid_url(url):
            result = urlparse(data)
            return all([result.scheme, result.netloc])

        if is_valid_url(data) is False:
            raise Exception(&amp;#39;Invalid URL.&amp;#39;)

        type_tiny = pyshorteners.Shortener()
        client = redis.StrictRedis(host=&amp;#39;localhost&amp;#39;, port=6379)

        if client.exists(data):
            return {&amp;#39;url&amp;#39;: client.get(data).decode(&amp;#39;utf-8&amp;#39;)}, 200

        else:
            if &amp;#39;tinyurl.com&amp;#39; in data:
                raise Exception(&amp;#39;URL is already shortened.&amp;#39;)

            short_url = type_tiny.tinyurl.short(data)
            client.set(short_url, data)
            return {&amp;#39;ur&amp;#39;: (short_url)}


api.add_resource(ApiResource, &amp;#39;/&amp;#39;)


if __name__ == &amp;#39;__main__&amp;#39;:
    app.run(debug=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do validation checks to ensure the URL is legit and not already shortened. If everything checks out, we shorten the URL and store the data in Redis. If the URL already exists in Redis, we return the shortened URL.&lt;/p&gt;
&lt;p&gt;The instrumentation we added earlier will &lt;a href=&quot;https://www.appsignal.com/tour/performance&quot;&gt;monitor the performance of the Redis calls&lt;/a&gt; and provide insights. Every Redis call is monitored and we can see the latencies, errors, and other metrics in our dashboard in AppSignal.&lt;/p&gt;
&lt;p&gt;To test this out, you can run the following CURL command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;gt; curl -i -X POST -H &amp;#39;Content-Type: application/json&amp;#39; -d &amp;#39;{&amp;quot;url&amp;quot;: &amp;quot;https://docs.appsignal.com/python/instrumentations/redis.html&amp;quot;}&amp;#39; http://127.0.0.1:5000
{&amp;quot;url&amp;quot;: &amp;quot;https://tinyurl.com/22ynqkyf&amp;quot;}
&amp;gt; curl -i -X POST -H &amp;#39;Content-Type: application/json&amp;#39; -d &amp;#39;{&amp;quot;url&amp;quot;: &amp;quot;https://tinyurl.com/22ynqkyf&amp;quot;}&amp;#39; http://127.0.0.1:5000
{&amp;quot;url&amp;quot;: &amp;quot;https://docs.appsignal.com/python/instrumentations/redis.html&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Feel free to repeat the command with different URLs to see how the URL shortener works. You can also check the Redis database to see the data stored there:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;gt; redis-cli GET https://tinyurl.com/22ynqkyf
&amp;quot;https://docs.appsignal.com/python/instrumentations/redis.html&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it! You have successfully &lt;a href=&quot;https://www.appsignal.com/python/redis-monitoring&quot;&gt;instrumented Redis in a Python application using AppSignal&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can now monitor the performance of your Redis calls and get insights, which we will cover in the next section.&lt;/p&gt;
&lt;h2&gt;Check Latencies in AppSignal&lt;/h2&gt;
&lt;p&gt;If you go to your AppSignal dashboard, you will see the performance metrics for API calls under &lt;a href=&quot;https://www.appsignal.com/tour/performance&quot;&gt;Performance&lt;/a&gt;:
&lt;img src=&quot;/images/blog/2025-04/python-appsignal-redis-0.png&quot; alt=&quot;Performance dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;There is an &lt;code&gt;Actions&lt;/code&gt; section that shows API call latencies. You can keep track of P95s, P90s, and other metrics to ensure your application is performing well. You can also set up alerts to notify you if the latencies are too high.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-04/python-appsignal-redis-1.png&quot; alt=&quot;API latencies&quot;/&gt;&lt;/p&gt;
&lt;p&gt;To check for Redis latencies, you can go to &lt;code&gt;Slow queries&lt;/code&gt; under &lt;code&gt;Performance&lt;/code&gt;. There, you will see the latencies of the Redis calls listed by command.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-04/python-appsignal-redis-2.png&quot; alt=&quot;Redis latencies&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The entire event is captured, including the command, the duration, and the backtrace.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-04/python-appsignal-redis-3.png&quot; alt=&quot;Redis event&quot;/&gt;&lt;/p&gt;
&lt;p&gt;To set up alerts, go to the &lt;a href=&quot;https://www.appsignal.com/tour/anomaly-detection&quot;&gt;anomaly detection section of AppSignal&lt;/a&gt;:
&lt;img src=&quot;/images/blog/2025-04/python-appsignal-redis-4.png&quot; alt=&quot;Anomaly detection&quot;/&gt;&lt;/p&gt;
&lt;p&gt;There, you can configure triggers for latencies in Redis calls. You can set up alerts for P95s and P90s to notify you if the latencies exceed a certain threshold.&lt;/p&gt;
&lt;p&gt;Simply click on the &lt;code&gt;Add Trigger&lt;/code&gt; button and configure the trigger:
&lt;img src=&quot;/images/blog/2025-04/python-appsignal-redis-5.png&quot; alt=&quot;Add trigger&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You can set a threshold, duration, and notification method for an alert.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.appsignal.com/python/redis-monitoring&quot;&gt;Read more about AppSignal&amp;#39;s instrumentation for Redis&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s that!&lt;/p&gt;
&lt;h1&gt;Wrapping Up&lt;/h1&gt;
&lt;p&gt;In this tutorial, we learned how to instrument Redis in a Python application using AppSignal. We created a simple URL shortener that stores data in a local instance of Redis and monitored the performance of the Redis calls in our dashboard in AppSignal. We also learned how to check Redis call latencies and set up alerts.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>An Introduction to Testing in Python Flask</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/04/02/an-introduction-to-testing-in-python-flask.html"/>
    <id>https://blog.appsignal.com/2025/04/02/an-introduction-to-testing-in-python-flask.html</id>
    <published>2025-04-02T00:00:00+00:00</published>
    <updated>2025-04-02T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s uncover why testing is important for Flask applications and how you can effectively implement tests.</summary>
    <content type="html">&lt;p&gt;So, you&amp;#39;ve built a Flask application — congratulations! You&amp;#39;ve crafted routes, connected databases, and perhaps even deployed it to a server. But have you tested it thoroughly?&lt;/p&gt;
&lt;p&gt;Testing isn&amp;#39;t just a checkbox on a developer&amp;#39;s to-do list: it&amp;#39;s an essential part of building robust and reliable applications.&lt;/p&gt;
&lt;p&gt;So, in this article, we&amp;#39;ll describe why testing is important for Flask applications and how you can effectively implement tests.&lt;/p&gt;
&lt;h2&gt;Why Write Tests for Python Flask?&lt;/h2&gt;
&lt;p&gt;But first of all, why should you test your Flask application?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;To ensure code quality&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;#39;s face it: bugs happen no matter how carefully you code, and the more features your application has, the greater the risk that a new change might introduce issues elsewhere. In this context, tests act as a safety net, catching problems before they make it to production. So, by writing tests for your Flask app, you can detect bugs early, ensuring that your code meets the quality standards you and your users expect.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;To streamline development&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Have you ever added a new feature and accidentally broken something else? Maybe that one &amp;quot;innocent&amp;quot; change caused an entire section of your app to malfunction. If you&amp;#39;ve experienced that frustration, automated tests are there to back you up. In fact, tests speed up development cycles by identifying issues as soon as they arise, especially when integrated into Continuous Integration (CI) processes.&lt;/p&gt;
&lt;p&gt;Also, CI helps run your tests automatically whenever there&amp;#39;s a new change in the codebase, ensuring that problems are identified and fixed before they accumulate. This means less time debugging and more time building the features your users will love.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;To boost confidence in changes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Making changes to an existing codebase can sometimes feel like walking on eggshells, so you might wonder: &amp;quot;Will this new feature break existing functionality?&amp;quot;. Well, a comprehensive test suite alleviates this anxiety because with well-written tests you can make changes confidently, knowing that if any breaking changes occur, they will be detected immediately. So, tests give you the assurance that your code is behaving correctly, even after refactoring or adding new features.&lt;/p&gt;
&lt;h2&gt;What to Test&lt;/h2&gt;
&lt;p&gt;Testing is an essential part of the development cycle, but it requires time and effort (often limited resources). Also, while testing everything can be overwhelming, it&amp;#39;s usually not practical or necessary. So where should you focus your efforts when testing a web application?&lt;/p&gt;
&lt;p&gt;Here are some key components of a Flask application that should be covered by tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unit tests for views and routes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your views and routes are the entry points to your application because they are where requests from users are handled. Unit tests should ensure that each route returns the correct HTTP response codes, renders the right templates, and provides the expected data. This kind of testing makes sure that individual parts of your application are working correctly in isolation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Testing business logic&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Business logic is the core of your application, as it includes calculations, data processing, and decision-making that empowers your application&amp;#39;s functionality.&lt;/p&gt;
&lt;p&gt;So, testing your business logic ensures that your helper functions, models, and services perform correctly across a variety of inputs, covering edge cases and typical scenarios. This prevents subtle bugs that may not be immediately visible in the UI.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Testing database interactions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your application interacts with a database (and let&amp;#39;s be honest, most apps do), testing those interactions is fundamental. In particular, you need to ensure that data is being correctly saved, retrieved, updated, and deleted. So, whether you&amp;#39;re using Flask-SQLAlchemy or another ORM, tests should cover your data&amp;#39;s integrity and verify that CRUD operations function as expected. Database tests can catch issues like incorrect queries, foreign key violations, and data inconsistencies.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Integration and functional tests&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unit tests are great, but they only verify the individual parts of your app in isolation. Integration tests take things a step further by ensuring that all these parts work correctly together. They verify that your routes, databases, and other components interact as expected.&lt;/p&gt;
&lt;p&gt;Functional tests go even further, as they simulate user interactions and workflows, such as submitting forms, handling authentication, and verifying permissions. This helps ensure that your application delivers a seamless experience for end-users.&lt;/p&gt;
&lt;h2&gt;A Popular Python Testing Framework: &lt;code&gt;pytest&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;When it comes to testing in Python, &lt;code&gt;pytest&lt;/code&gt; represents a modern and concise framework very popular among developers. It&amp;#39;s simple to use, yet powerful enough to handle complex testing needs. With its straightforward syntax and extensive plugin ecosystem, &lt;code&gt;pytest&lt;/code&gt; makes writing tests less of a chore and more of an integral part of development.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pytest&lt;/code&gt; also provides several useful features, such as fixtures, parameterized tests, and assertion introspection, which make testing more efficient and powerful for developers.&lt;/p&gt;
&lt;h3&gt;Fixtures&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pytest.org/en/6.2.x/fixture.html&quot;&gt;Fixtures&lt;/a&gt; allow you to set up a specific state for your tests, like initializing a database or creating necessary test data. This makes your tests cleaner by handling repetitive setup tasks, allowing you to focus on the actual test logic rather than boilerplate code.&lt;/p&gt;
&lt;p&gt;For example, if you need to create a test client or seed a database before each test, fixtures can do this automatically.&lt;/p&gt;
&lt;h3&gt;Parameterized Tests&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pytest.org/en/stable/example/parametrize.html&quot;&gt;Parameterized tests&lt;/a&gt; let you run the same test function with different inputs, which can significantly reduce code duplication. This helps ensure that your function works correctly across a range of inputs without you needing to write multiple test functions manually.&lt;/p&gt;
&lt;h3&gt;Assertion introspection&lt;/h3&gt;
&lt;p&gt;When an &lt;a href=&quot;https://docs.pytest.org/en/stable/how-to/assert.html&quot;&gt;assertion&lt;/a&gt; fails, &lt;code&gt;pytest&lt;/code&gt; provides detailed information about what went wrong. Instead of just telling you that an assertion failed, it shows the values involved, making debugging much easier. This means you spend less time figuring out why a test failed and more time fixing the problem.&lt;/p&gt;
&lt;h2&gt;Best Practices for Testing in Flask&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s now provide some guidelines on best practices for testing Flask applications.&lt;/p&gt;
&lt;h3&gt;Maintain Readable Tests&lt;/h3&gt;
&lt;p&gt;Your tests are code too, and they deserve the same care and attention as your application code. So, write readable and self-explanatory test cases by using meaningful names for test functions and variables, and structuring your tests logically.&lt;/p&gt;
&lt;p&gt;As a rule of thumb, a test case should ideally tell a story: &amp;quot;Given this input, the application should behave like this&amp;quot;, so avoid overly complex or tightly coupled tests that are hard to understand and maintain. Readable tests are easier to debug and maintain, and they help other developers (or future you) understand what you&amp;#39;re trying to verify.&lt;/p&gt;
&lt;h3&gt;Keep Tests Isolated&lt;/h3&gt;
&lt;p&gt;Isolation is key in testing web applications. In particular, each test should run independently of other ones to avoid side effects and ensure accurate results. For example, changes made to a database during one test shouldn&amp;#39;t affect another; this means, for example, resetting the database between tests or using mock objects. Isolation, in fact, makes it easier to identify the root cause of any issues and prevents a test from passing or failing due to external factors.&lt;/p&gt;
&lt;h3&gt;Use Mocking for External Dependencies&lt;/h3&gt;
&lt;p&gt;Your Flask application may rely on external services like third-party APIs, payment gateways, or microservices. Testing these dependencies directly can lead to flaky tests due to network issues, service downtime, or rate limits. By mocking, you can simulate the behavior of these external services, allowing your tests to be fast, reliable, and independent of external factors.&lt;/p&gt;
&lt;h3&gt;Focus on Edge Cases and Error Handling&lt;/h3&gt;
&lt;p&gt;While it&amp;#39;s important to test the expected &amp;quot;happy path&amp;quot; scenarios, it&amp;#39;s equally important to test how your application handles edge cases and errors. This includes testing with invalid inputs, missing data, or unexpected user behavior. So, thorough testing of edge cases ensures your application is robust, user-friendly, and can gracefully handle exceptions without crashing or exposing sensitive information.&lt;/p&gt;
&lt;h2&gt;Practical Examples of Testing in Flask&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s now go through practical examples of how to test a Flask application, by providing different scenarios.&lt;/p&gt;
&lt;h3&gt;Setting Up Your Flask App for Testing&lt;/h3&gt;
&lt;p&gt;First, to make testing easier, it&amp;#39;s important to structure your repository in a logical way. Here&amp;#39;s a common structure for a Flask project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;my_flask_app/
├── app/
│   ├── __init__.py
│   ├── routes.py
│   └── models.py
├── tests/
│   ├── __init__.py
│   ├── test_routes.py
│   └── test_models.py
├── venv/
├── requirements.txt
├── config.py
└── run.py
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app/&lt;/code&gt;: This directory contains your main application code, including routes, models, and other core functionality.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tests/&lt;/code&gt;: This directory contains all your test files. Each major component of your application should have corresponding test files to ensure comprehensive coverage.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;venv/&lt;/code&gt;: The virtual environment directory. This folder should typically not be committed to version control (&lt;code&gt;.gitignore&lt;/code&gt; it).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requirements.txt&lt;/code&gt;: A file that lists all dependencies for your project.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run.py&lt;/code&gt;: The script used to start your Flask application.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By following this structure, you create a clean separation between your application code and your test code, which helps in maintaining and scaling your project effectively.&lt;/p&gt;
&lt;p&gt;Then, you can create a virtual environment to manage dependencies for your Flask project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On Windows&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;&amp;gt; python -m venv venv
&amp;gt; venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;On macOS/Linux&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ python3 -m venv venv
$ source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Assuming you&amp;#39;ve already installed Flask, you can now install &lt;code&gt;pytest&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install pytest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then ensure your Flask app is structured in a way that makes testing feasible. Typically, this means your app should be an importable module. Here&amp;#39;s a basic structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Flask

def create_app():
    app = Flask(__name__)

    @app.route(&amp;#39;/&amp;#39;)
    def index():
        return &amp;#39;Hello, World!&amp;#39;

    return app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This structure allows you to easily create multiple instances of your Flask app, which is helpful for testing.&lt;/p&gt;
&lt;p&gt;We can now go through some basic testing examples.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h3&gt;Writing Unit Tests for Routes&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s write a test for the &lt;code&gt;/&lt;/code&gt; route using &lt;code&gt;pytest&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pytest
from app import create_app

@pytest.fixture
def client():
    app = create_app()
    app.config[&amp;#39;TESTING&amp;#39;] = True

    with app.test_client() as client:
        yield client

def test_index_route(client):
    response = client.get(&amp;#39;/&amp;#39;)
    assert response.status_code == 200
    assert b&amp;#39;Hello, World!&amp;#39; in response.data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this test:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We use a &lt;code&gt;pytest&lt;/code&gt; fixture to create a test client, which allows us to simulate HTTP requests to our Flask app without starting the server.&lt;/li&gt;
&lt;li&gt;We set the app configuration to &lt;code&gt;&amp;#39;TESTING&amp;#39; = True&lt;/code&gt; to indicate that we&amp;#39;re running in a test environment, which can help with debugging and error handling.&lt;/li&gt;
&lt;li&gt;We send a GET request to the &lt;code&gt;/&lt;/code&gt; route.&lt;/li&gt;
&lt;li&gt;We assert that the response status code is &lt;code&gt;200 OK&lt;/code&gt;, and that the response data contains the expected text &lt;code&gt;&amp;#39;Hello, World!&amp;#39;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Testing Forms and POST Requests&lt;/h3&gt;
&lt;p&gt;Suppose you have a simple form that submits a user&amp;#39;s name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&amp;#39;/greet&amp;#39;, methods=[&amp;#39;POST&amp;#39;])
def greet():
    name = request.form[&amp;#39;name&amp;#39;]
    return f&amp;#39;Hello, {name}!&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s how you can test the form submission:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# test_app.py
def test_greet_route(client):
    response = client.post(&amp;#39;/greet&amp;#39;, data={&amp;#39;name&amp;#39;: &amp;#39;Alice&amp;#39;})
    assert response.status_code == 200
    assert b&amp;#39;Hello, Alice!&amp;#39; in response.data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this test:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We send a POST request to the &lt;code&gt;/greet&lt;/code&gt; endpoint with form data route (&lt;code&gt;{&amp;#39;name&amp;#39;: &amp;#39;Alice&amp;#39;}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;We assert that the response is successful (&lt;code&gt;status_code == 200&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;We verify that the response includes the personalized greeting (&lt;code&gt;&amp;#39;Hello, Alice!&amp;#39;&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Writing Parameterized Tests&lt;/h3&gt;
&lt;p&gt;Suppose we want to test the &lt;code&gt;/greet&lt;/code&gt; route with multiple names to ensure it handles different inputs correctly. We can use parameterized tests for this case, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pytest

@pytest.mark.parametrize(&amp;quot;name, expected_greeting&amp;quot;, [
    (&amp;quot;Alice&amp;quot;, b&amp;quot;Hello, Alice!&amp;quot;),
    (&amp;quot;Bob&amp;quot;, b&amp;quot;Hello, Bob!&amp;quot;),
    (&amp;quot;Charlie&amp;quot;, b&amp;quot;Hello, Charlie!&amp;quot;)
])

def test_greet_route_parametrized(client, name, expected_greeting):
    response = client.post(&amp;#39;/greet&amp;#39;, data={&amp;#39;name&amp;#39;: name})
    assert response.status_code == 200
    assert expected_greeting in response.data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this parameterized test, we use the &lt;code&gt;@pytest.mark.parametrize&lt;/code&gt; decorator to run the same test function with different inputs (&lt;code&gt;name&lt;/code&gt;) and expected outputs (&lt;code&gt;expected_greeting&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This helps ensure that the &lt;code&gt;/greet&lt;/code&gt; route works correctly for various names without duplicating code.&lt;/p&gt;
&lt;h3&gt;Testing Database Interactions with Test Isolation&lt;/h3&gt;
&lt;p&gt;As discussed above, testing database interactions is very important for applications that rely on data storage and retrieval. In this case, you should maintain isolation in testing as it ensures that each test runs in a clean environment, unaffected by the outcomes of other tests.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s show how we can provide isolation while testing a database.&lt;/p&gt;
&lt;h4&gt;Setting Up the Database for Testing&lt;/h4&gt;
&lt;p&gt;Suppose you have a simple &lt;code&gt;User&lt;/code&gt; model in a Flask application using SQLAlchemy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# models.py
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)

    def __repr__(self):
        return f&amp;#39;&amp;lt;User {self.username}&amp;gt;&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And in the &lt;code&gt;create_app&lt;/code&gt; function, you can initialize the database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# app/__init__.py
from flask import Flask
from .models import db, User

def create_app():
    app = Flask(__name__)
    app.config[&amp;#39;SQLALCHEMY_DATABASE_URI&amp;#39;] = &amp;#39;sqlite:///:memory:&amp;#39;
    app.config[&amp;#39;SQLALCHEMY_TRACK_MODIFICATIONS&amp;#39;] = False

    db.init_app(app)

    @app.route(&amp;#39;/add_user/&amp;lt;username&amp;gt;&amp;#39;)
    def add_user(username):
        user = User(username=username)
        db.session.add(user)
        db.session.commit()
        return f&amp;#39;User {username} added.&amp;#39;

    @app.route(&amp;#39;/users&amp;#39;)
    def list_users():
        users = User.query.all()
        return &amp;#39;, &amp;#39;.join([user.username for user in users])

    return app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We use an in-memory SQLite database (&lt;code&gt;sqlite:///:memory:&lt;/code&gt;) for testing purposes.&lt;/li&gt;
&lt;li&gt;We have two routes: one to add a user and another to list all users.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Writing Tests with Isolation&lt;/h4&gt;
&lt;p&gt;To ensure test isolation, you can set up the database afresh for each test. Here&amp;#39;s how you can do it using fixtures in &lt;code&gt;pytest&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# tests/test_models.py
import pytest
from app import create_app
from app.models import db, User

@pytest.fixture
def app():
    app = create_app()
    app.config[&amp;#39;TESTING&amp;#39;] = True
    app.config[&amp;#39;SQLALCHEMY_DATABASE_URI&amp;#39;] = &amp;#39;sqlite:///:memory:&amp;#39;

    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()

def test_add_user(client):
    response = client.get(&amp;#39;/add_user/Alice&amp;#39;)
    assert response.status_code == 200
    assert b&amp;#39;User Alice added.&amp;#39; in response.data

    response = client.get(&amp;#39;/users&amp;#39;)
    assert b&amp;#39;Alice&amp;#39; in response.data

def test_user_list_is_empty(client):
    response = client.get(&amp;#39;/users&amp;#39;)
    assert response.status_code == 200
    assert response.data == b&amp;#39;&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In these tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;app&lt;/code&gt; Fixture sets up the Flask application and initializes the database for each test. In particular, by using &lt;code&gt;db.create_all()&lt;/code&gt; and &lt;code&gt;db.drop_all()&lt;/code&gt;, we ensure that each test has a fresh database.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;client&lt;/code&gt; Fixture provides a test client for sending HTTP requests to our Flask app.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_add_user&lt;/code&gt; tests that a user can be added and then retrieved from the database.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_user_list_is_empty&lt;/code&gt; tests that the user list is empty when no users have been added.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By dropping and recreating the database between tests, we ensure that tests do not affect each other&amp;#39;s outcomes, achieving test isolation.&lt;/p&gt;
&lt;h4&gt;Final Advice on Database Testing: Cleaning Up After Tests&lt;/h4&gt;
&lt;p&gt;Always ensure that any resources initialized during a test are properly cleaned up afterward to save resources. In the example above, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Used &lt;code&gt;db.session.remove()&lt;/code&gt; to remove the database session. This method removes the current SQLAlchemy session associated with an application context, ensuring that any pending transactions are rolled back, and the session is removed from the session registry. Note that if you don&amp;#39;t remove the session after a test, the session might retain state (like uncommitted transactions) that could affect subsequent tests.&lt;/li&gt;
&lt;li&gt;Dropped all tables after the tests using &lt;code&gt;db.drop_all()&lt;/code&gt;. This method removes all schema objects (tables, indexes, constraints), returning the database to an empty state. This guarantees that each test starts with no pre-existing data or schema changes that could influence its behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of the consequences of not cleaning up after testing are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Interdependent tests&lt;/strong&gt;: Tests might pass or fail depending on the order they are run, leading to unreliable test results.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource exhaustion&lt;/strong&gt;: Open sessions or connections can accumulate, potentially causing the database or application to run out of resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hard-to-debug failures&lt;/strong&gt;: Side effects from previous tests can cause failures in unrelated tests, making debugging more difficult.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Using Mocking for External Dependencies&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s consider a common scenario — your application sends emails using an external email service when users register:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# app/routes.py
from flask import Blueprint, request, render_template
import external_email_service

bp = Blueprint(&amp;#39;routes&amp;#39;, __name__)

@bp.route(&amp;#39;/register&amp;#39;, methods=[&amp;#39;GET&amp;#39;, &amp;#39;POST&amp;#39;])
def register():
    if request.method == &amp;#39;POST&amp;#39;:
        email = request.form[&amp;#39;email&amp;#39;]
        # Assume user registration logic here
        result = external_email_service.send_welcome_email(email)
        if result:
            return &amp;#39;Registration successful!&amp;#39;, 200
        else:
            return &amp;#39;Failed to send welcome email.&amp;#39;, 500
    return render_template(&amp;#39;register.html&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We could test it with mocking like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# tests/test_routes.py
import pytest
from unittest.mock import patch
from app import create_app

@pytest.fixture
def client():
    app = create_app()
    app.config[&amp;#39;TESTING&amp;#39;] = True

    with app.test_client() as client:
        yield client

@patch(&amp;#39;app.routes.external_email_service&amp;#39;)
def test_register_route_success(mock_email_service, client):
    mock_email_service.send_welcome_email.return_value = True

    response = client.post(&amp;#39;/register&amp;#39;, data={&amp;#39;email&amp;#39;: &amp;#39;user@example.com&amp;#39;})
    assert response.status_code == 200
    assert b&amp;#39;Registration successful!&amp;#39; in response.data
    mock_email_service.send_welcome_email.assert_called_once_with(&amp;#39;user@example.com&amp;#39;)

@patch(&amp;#39;app.routes.external_email_service&amp;#39;)
def test_register_route_email_failure(mock_email_service, client):
    mock_email_service.send_welcome_email.return_value = False

    response = client.post(&amp;#39;/register&amp;#39;, data={&amp;#39;email&amp;#39;: &amp;#39;user@example.com&amp;#39;})
    assert response.status_code == 500
    assert b&amp;#39;Failed to send welcome email.&amp;#39; in response.data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this test:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We use &lt;code&gt;unittest.mock.patch&lt;/code&gt; to replace the &lt;code&gt;send_welcome_email&lt;/code&gt; function with a mock that simulates the external email service.&lt;/li&gt;
&lt;li&gt;We test both scenarios: where the email is sent successfully and when the email service fails (&lt;code&gt;response.status_code == 200&lt;/code&gt; and &lt;code&gt;response.status_code == 500&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;We assert that the mock was called with the correct parameters, ensuring our code interacts with the external service as expected.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: The &lt;code&gt;unittest.mock&lt;/code&gt; module is included in the standard library from Python 3.3 and above.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Focusing on Edge Cases and Error Handling&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s consider another common scenario — your web application provides a login route that should handle invalid credentials gracefully. This could be coded like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# app/routes.py
@bp.route(&amp;#39;/login&amp;#39;, methods=[&amp;#39;GET&amp;#39;, &amp;#39;POST&amp;#39;])
def login():
    if request.method == &amp;#39;POST&amp;#39;:
        username = request.form[&amp;#39;username&amp;#39;]
        password = request.form[&amp;#39;password&amp;#39;]
        # Assume user authentication logic here
        if username == &amp;#39;&amp;#39; or password == &amp;#39;&amp;#39;:
            return &amp;#39;Username and password are required.&amp;#39;, 400
        if not authenticate_user(username, password):
            return &amp;#39;Invalid credentials.&amp;#39;, 401
        return &amp;#39;Login successful!&amp;#39;, 200
    return render_template(&amp;#39;login.html&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s how we could test it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# tests/test_routes.py
def test_login_missing_credentials(client):
    response = client.post(&amp;#39;/login&amp;#39;, data={&amp;#39;username&amp;#39;: &amp;#39;&amp;#39;, &amp;#39;password&amp;#39;: &amp;#39;&amp;#39;})
    assert response.status_code == 400
    assert b&amp;#39;Username and password are required.&amp;#39; in response.data

def test_login_invalid_credentials(client):
    response = client.post(&amp;#39;/login&amp;#39;, data={&amp;#39;username&amp;#39;: &amp;#39;user&amp;#39;, &amp;#39;password&amp;#39;: &amp;#39;wrongpass&amp;#39;})
    assert response.status_code == 401
    assert b&amp;#39;Invalid credentials.&amp;#39; in response.data

def test_login_success(client):
    # Assuming &amp;#39;authenticate_user&amp;#39; will return True for these credentials during testing
    with patch(&amp;#39;app.routes.authenticate_user&amp;#39;, return_value=True):
        response = client.post(&amp;#39;/login&amp;#39;, data={&amp;#39;username&amp;#39;: &amp;#39;user&amp;#39;, &amp;#39;password&amp;#39;: &amp;#39;pass&amp;#39;})
        assert response.status_code == 200
        assert b&amp;#39;Login successful!&amp;#39; in response.data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, we ensure the application responds appropriately to different user inputs by testing different scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We test the case where the username and password are missing, expecting a &lt;code&gt;400 Bad Request&lt;/code&gt; response.&lt;/li&gt;
&lt;li&gt;We test with incorrect credentials, expecting a &lt;code&gt;401 Unauthorized&lt;/code&gt; response.&lt;/li&gt;
&lt;li&gt;We mock the &lt;code&gt;authenticate_user&lt;/code&gt; function to return &lt;code&gt;True&lt;/code&gt; and test a successful login.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that&amp;#39;s it!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Testing isn&amp;#39;t just about finding bugs; it&amp;#39;s about building confidence in your code and making your development process more efficient and reliable.&lt;/p&gt;
&lt;p&gt;By writing tests for your Flask application, you ensure code quality, streamline development, and can make changes without fear of breaking things. By using tools like &lt;code&gt;pytest&lt;/code&gt; and best practices such as test isolation and readability, adding tests to your workflow becomes a manageable and rewarding task.&lt;/p&gt;
&lt;p&gt;The next time you find yourself hesitating to write tests, remember: the effort you invest in testing today
will save you countless hours and headaches in the future, making your Flask application robust, maintainable, and enjoyable to work on.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>An Introduction to Flask-SQLAlchemy in Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/02/26/an-introduction-to-flask-sqlalchemy-in-python.html"/>
    <id>https://blog.appsignal.com/2025/02/26/an-introduction-to-flask-sqlalchemy-in-python.html</id>
    <published>2025-02-26T00:00:00+00:00</published>
    <updated>2025-02-26T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">In this article, we&#039;ll introduce SQLAlchemy and Flask-SQLAlchemy, highlighting their key features.</summary>
    <content type="html">&lt;p&gt;Efficient management of database interactions is one of the most important tasks when building Python web applications. SQLAlchemy — a comprehensive SQL toolkit and Object-Relational Mapping (ORM) library — is widely used in the Python ecosystem for this purpose.&lt;/p&gt;
&lt;p&gt;However, integrating SQLAlchemy with Flask can be a complex process. So developers created Flask-SQLAlchemy to provide a seamless way to use SQLAlchemy in Flask applications, making database management a straightforward process.&lt;/p&gt;
&lt;p&gt;In this article, we introduce SQLAlchemy and Flask-SQLAlchemy, highlighting their key features. We&amp;#39;ll also create a basic Flask app to use Flask-SQLAlchemy for database management.&lt;/p&gt;
&lt;h2&gt;What is SQLAlchemy?&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s start by describing what SQLAlchemy is and what its strengths are.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sqlalchemy.org/&quot;&gt;SQLAlchemy&lt;/a&gt; is an open source SQL toolkit and ORM library for Python, created to let objects be objects, and tables be tables. It provides developers with tools to work with databases such as Oracle, DB2, MySQL, PostgreSQL, and SQLite at both high and low levels of abstraction.&lt;/p&gt;
&lt;p&gt;Its two primary components are the core and ORM:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.sqlalchemy.org/en/20/core/&quot;&gt;SQLAlchemy Core&lt;/a&gt;&lt;/strong&gt;: This provides a full suite of well-organized database interaction tools. In particular, it&amp;#39;s a low-level SQL toolkit that offers an abstraction layer allowing developers to work directly with relational databases by using Python. It contains the so-called &amp;quot;SQL Expression Language&amp;quot;, a toolkit on its own that provides a system for constructing SQL expressions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.sqlalchemy.org/en/20/orm/index.html&quot;&gt;SQLAlchemy ORM&lt;/a&gt;&lt;/strong&gt;: The ORM layer builds on top of the Core and provides a higher-level, more abstracted interface for interacting with databases. In practice, it maps Python classes to database tables and instances of these classes to rows in those tables, enabling developers to work with database records as if they were Python objects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;#39;s a high-level schema of these two components:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-02/overview.png&quot; alt=&quot;Overview of SQLAlchemy&quot;/&gt;&lt;/p&gt;
&lt;p&gt;So, basically, if you like writing SQL directly and want fine-grained control over database interactions, then Core is for you. With the ORM, you do not need to write SQL clauses: it allows you to define database models as Python classes and interact with them by using object-oriented programming concepts like inheritance and relationships.&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s look at both in more detail.&lt;/p&gt;
&lt;h3&gt;Using SQLAlchemy ORM&lt;/h3&gt;
&lt;p&gt;One of the primary features of SQLAlchemy ORM is its ability to map database tables to Python classes seamlessly, allowing developers to manipulate database records using object-oriented paradigms. This approach simplifies database interactions by enabling you to work directly with Python objects instead of writing SQL queries, making code more maintainable and readable.&lt;/p&gt;
&lt;p&gt;The ORM also provides a rich set of tools for managing relationships between entities, handling complex queries, and automatically synchronizing the state of in-memory objects with the database. Additionally, SQLAlchemy ORM supports &lt;a href=&quot;https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html&quot;&gt;lazy loading and eager loading&lt;/a&gt;, offering efficient data retrieval mechanisms tailored to specific use cases. These features, combined with the ability to declaratively define schemas, make the ORM a powerful tool for developers seeking to integrate databases into their Python applications with minimal boilerplate.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s how to create a table, insert a record, and query data with the ORM:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker

# Define the database engine
engine = create_engine(&amp;#39;sqlite:///:memory:&amp;#39;, echo=True)

# Define the base class for ORM models
Base = declarative_base()

# Define the ORM model
class User(Base):
    __tablename__ = &amp;#39;users&amp;#39;

    id = Column(Integer, primary_key=True)
    name = Column(String)
    age = Column(Integer)

# Create the table in the database
Base.metadata.create_all(engine)

# Create a session to interact with the database
Session = sessionmaker(bind=engine)
session = Session()

# Insert a new user
new_user = User(name=&amp;quot;John Doe&amp;quot;, age=30)
session.add(new_user)
session.commit()

# Query the database
user = session.query(User).filter_by(name=&amp;quot;John Doe&amp;quot;).first()
print(f&amp;quot;User: {user.name}, Age: {user.age}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now onto SQLAlchemy Core.&lt;/p&gt;
&lt;h3&gt;Using SQLAlchemy Core&lt;/h3&gt;
&lt;p&gt;SQLAlchemy Core, on the other hand, offers a lower-level approach that provides direct control over database operations, providing features like schema definition, a powerful SQL expression language, and the engine for database connectivity.&lt;/p&gt;
&lt;p&gt;The schema/types system in SQLAlchemy Core allows developers to define the structure of their database tables explicitly, including data types, constraints, and indexes, using Python constructs. This ensures a consistent and clear definition of the database schema that can be programmatically manipulated.&lt;/p&gt;
&lt;p&gt;The SQL expression language is the central feature of SQLAlchemy Core, enabling developers to build SQL queries dynamically and expressively using Python. This language allows for the construction of complex queries and data manipulations while maintaining full control over the generated SQL, making it ideal for developers who need fine-grained control over their database interactions.&lt;/p&gt;
&lt;p&gt;Finally, the engine in SQLAlchemy Core is responsible for establishing and managing connections to the database, executing SQL statements, and handling transactions. It provides a robust and flexible foundation for working with various database backends, offering both synchronous and asynchronous interaction modes.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s how to create a table, insert a record, and query data using the Core:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, insert, select

# Define the database engine
engine = create_engine(&amp;#39;sqlite:///:memory:&amp;#39;, echo=True)

# Define the metadata object for schema management
metadata = MetaData()

# Define the table using SQLAlchemy Core
users_table = Table(
    &amp;#39;users&amp;#39;, metadata,
    Column(&amp;#39;id&amp;#39;, Integer, primary_key=True),
    Column(&amp;#39;name&amp;#39;, String),
    Column(&amp;#39;age&amp;#39;, Integer)
)

# Create the table in the database
metadata.create_all(engine)

# Insert a new user using Core
with engine.connect() as connection:
    insert_stmt = users_table.insert().values(name=&amp;quot;John Doe&amp;quot;, age=30)
    connection.execute(insert_stmt)

# Query the database using Core
with engine.connect() as connection:
    select_stmt = select(users_table).where(users_table.c.name == &amp;quot;John Doe&amp;quot;)
    result = connection.execute(select_stmt)
    user = result.fetchone()
    print(f&amp;quot;User: {user.name}, Age: {user.age}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let&amp;#39;s define what Flask-SQLAlchemy is and why you should use it.&lt;/p&gt;
&lt;h2&gt;What is Flask-SQLAlchemy in Python?&lt;/h2&gt;
&lt;p&gt;Flask-SQLAlchemy is a Flask extension that integrates SQLAlchemy with Flask, simplifying the process of database management in Flask applications. It combines the power and flexibility of SQLAlchemy with the simplicity and ease of use that Flask developers expect.&lt;/p&gt;
&lt;p&gt;By providing a layer of abstraction over SQLAlchemy, in fact, Flask-SQLAlchemy makes it easier to set up, configure, and manage databases in Flask-based projects.&lt;/p&gt;
&lt;h3&gt;Key Features of Flask-SQLAlchemy for Python&lt;/h3&gt;
&lt;p&gt;Flask-SQLAlchemy comes with several key features that make it an excellent choice for database management in Flask applications, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Easy configuration&lt;/strong&gt;: Flask-SQLAlchemy simplifies the configuration of SQLAlchemy with Flask. You can configure your database connection and other settings through Flask’s configuration system, making it easy to manage different environments (for example: development, testing, and production environments).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic tables creation&lt;/strong&gt;: If you are connecting to a database that already has tables, Flask-SQLAlchemy can automatically create tables based on your defined models when the application starts. This reduces the need for manual table creation and synchronization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration with Flask’s application context&lt;/strong&gt;: Flask-SQLAlchemy leverages Flask&amp;#39;s app context, ensuring that your database session is properly managed across requests. This simplifies transaction management and ensures that your database operations are handled correctly in a web environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplified querying&lt;/strong&gt;: With Flask-SQLAlchemy, querying the database becomes straightforward, as it extends SQLAlchemy’s query interface to work seamlessly within Flask. This allows you to use familiar Flask patterns when interacting with your database.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How Flask-SQLAlchemy Simplifies Database Management&lt;/h3&gt;
&lt;p&gt;Flask-SQLAlchemy reduces the complexity of setting up and using SQLAlchemy with Flask by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Streamlining configuration and setup&lt;/strong&gt;: With Flask-SQLAlchemy, you don’t need to manually initialize and configure SQLAlchemy. The extension takes care of this by automatically binding SQLAlchemy to your Flask application and using Flask’s configuration to manage database settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Providing simplified session management&lt;/strong&gt;: In a web application, managing database sessions can be tricky. Flask-SQLAlchemy automatically manages the database session for each request, ensuring that sessions are properly opened and closed, reducing the risk of transaction errors and memory leaks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enhancing development speed&lt;/strong&gt;: By abstracting away much of the boilerplate code needed to set up and interact with a database, Flask-SQLAlchemy allows developers to focus more on building features rather than on database management. The extension’s tight integration with Flask’s development ecosystem also means that it plays well with other Flask extensions and tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up Flask-SQLAlchemy&lt;/h2&gt;
&lt;p&gt;So, now that we&amp;#39;ve presented Flask-SQLAlchemy, let&amp;#39;s provide a practical example of how to use it in a Flask application.&lt;/p&gt;
&lt;p&gt;You can find all the code stored in &lt;a href=&quot;https://github.com/federico-trotta/flask-sqlalchemy_example&quot;&gt;this public repo&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;To replicate this example, your system must satisfy the following prerequisites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu (any recent version) for Windows Subsystem for Linux (WSL): The steps in this tutorial are tailored for Ubuntu in WSL, but they are adaptable to other Linux distributions, especially not under WLS, or for different Operating Systems (but you&amp;#39;ll need to adapt the code).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Python 3.6+&lt;/code&gt; and &lt;code&gt;pip&lt;/code&gt;: Flask-SQLAlchemy requires &lt;code&gt;Python 3.6&lt;/code&gt; or higher. We’ll also install everything we need with &lt;code&gt;pip&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;MySQL: The steps in this guide are tested with MySQL installed (on Ubuntu for WSL).&lt;/li&gt;
&lt;li&gt;Basic knowledge of Python and Flask: This tutorial requires familiarity with virtual environments, Flask application structure, and basic terminal commands.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Requirements&lt;/h3&gt;
&lt;p&gt;For Flask to do its job, we first need to create a new database in MySQL.&lt;/p&gt;
&lt;p&gt;If you are unfamiliar with it, you can follow this guide.&lt;/p&gt;
&lt;h4&gt;Step 1: Launch MySQL&lt;/h4&gt;
&lt;p&gt;Launch MySQL by typing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sudo service mysql start
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: This is particular for Ubuntu on WSL. On Ubuntu (not under WSL) the command changes (&lt;code&gt;$ sudo systemctl mysql start&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h4&gt;Step 2: Access MySQL&lt;/h4&gt;
&lt;p&gt;Access MySQL by typing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ mysql -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Here you&amp;#39;ll be asked to insert the password you set when you installed MySQL.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Step 3: Create the Database&lt;/h4&gt;
&lt;p&gt;Everything is now set up to create a database. You can create it like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;CREATE DATABASE flask_db;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: You will have to insert this database name in the Flask app later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then, create a user and set its password:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;CREATE USER &amp;#39;flaskuser&amp;#39;@&amp;#39;localhost&amp;#39; IDENTIFIED BY &amp;#39;your_password&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: You will have to insert this user and password in the Flask app later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Finally, grant privileges to the user:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GRANT ALL PRIVILEGES ON flask_db.* TO &amp;#39;flaskuser&amp;#39;@&amp;#39;localhost&amp;#39;;

FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Installation and Configuration&lt;/h3&gt;
&lt;p&gt;Now, let&amp;#39;s install all the necessary libraries for using Flask and create a web app example using FlaskSQL-Alchemy.&lt;/p&gt;
&lt;h4&gt;Step 1: Create the Project Directory&lt;/h4&gt;
&lt;p&gt;First, create the main folder that will host the project via the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ mkdir flask_sqlalchemy_example
$ cd flask_sqlalchemy_example
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 2: Create and Activate a Virtual Environment&lt;/h4&gt;
&lt;p&gt;Before installing the needed libraries, we suggest creating a virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ python3 -m venv venv
$ source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 3: Install Flask, Flask-SQLAlchemy, and Other Libraries&lt;/h4&gt;
&lt;p&gt;You can now install everything that&amp;#39;s needed for this tutorial:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ pip install Flask Flask-SQLAlchemy PyMySQL cryptography Flask-WTF
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 4: Create the Structure of the Project&lt;/h4&gt;
&lt;p&gt;Finally, to recreate the demo project, create the following project structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;flask_sqlalchemy_example/
│
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   ├── forms.py
│   ├── templates/
│       └── index.html
│
├── venv/
│   └── ... (virtual environment files)
│
├── config.py
└── run.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create the Flask App&lt;/h3&gt;
&lt;p&gt;Now everything is set up to create the Flask app.&lt;/p&gt;
&lt;p&gt;The application we&amp;#39;re creating leverages Flask-SQLAlchemy to build a lightweight web application that handles user data management. In particular, we&amp;#39;ll create a web app where users can compile a form and input their username and email address.&lt;/p&gt;
&lt;p&gt;Once they fill out the form and submit it, the application validates the data. If the input is valid, the app adds a new user record to the MySQL database.&lt;/p&gt;
&lt;h4&gt;Step 1: Create the &lt;code&gt;config.py&lt;/code&gt; File&lt;/h4&gt;
&lt;p&gt;First, create the &lt;code&gt;config.py&lt;/code&gt; file to hold the application configuration, including the database URI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
    SECRET_KEY = os.environ.get(&amp;#39;SECRET_KEY&amp;#39;) or &amp;#39;a_very_secret_key&amp;#39;
    SQLALCHEMY_DATABASE_URI = &amp;#39;mysql+pymysql://flaskuser:your_password@localhost/flask_db&amp;#39;
    SQLALCHEMY_TRACK_MODIFICATIONS = False
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Here you have to use the user, password, and database name set when you created the database in MySQL.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Step 2: Create the &lt;code&gt;__init__.py&lt;/code&gt; File&lt;/h4&gt;
&lt;p&gt;Create the &lt;code&gt;__init__.py&lt;/code&gt; file inside the &lt;code&gt;app/&lt;/code&gt; directory to initialize the Flask app and SQLAlchemy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from config import Config

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)

    with app.app_context():
        from . import routes, models
        db.create_all()
        routes.register_routes(app)

    return app
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 3: Create a Model&lt;/h4&gt;
&lt;p&gt;Now, let&amp;#39;s define a model representing a user in &lt;code&gt;app/models.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from app import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __repr__(self):
        return f&amp;#39;&amp;lt;User {self.username}&amp;gt;&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a direct representation of a database table within SQLAlchemy’s ORM, allowing for efficient querying and manipulation of user records.&lt;/p&gt;
&lt;h4&gt;Step 4: Define a Basic Route&lt;/h4&gt;
&lt;p&gt;In &lt;code&gt;app/routes.py&lt;/code&gt;, create a route to display a list of users:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import render_template, redirect, url_for, flash
from app import db
from app.models import User
from app.forms import UserForm

def register_routes(app):
    @app.route(&amp;#39;/&amp;#39;, methods=[&amp;#39;GET&amp;#39;, &amp;#39;POST&amp;#39;])
    def index():
        form = UserForm()
        if form.validate_on_submit():
            username = form.username.data
            email = form.email.data
            user = User(username=username, email=email)
            db.session.add(user)
            db.session.commit()
            flash(&amp;#39;User added successfully!&amp;#39;, &amp;#39;success&amp;#39;)
            return redirect(url_for(&amp;#39;index&amp;#39;))

        users = User.query.all()
        return render_template(&amp;#39;index.html&amp;#39;, form=form, users=users)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 5: Create a form&lt;/h4&gt;
&lt;p&gt;Create a form for users in the file &lt;code&gt;app/forms.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Email, Length

class UserForm(FlaskForm):
    username = StringField(&amp;#39;Username&amp;#39;, validators=[DataRequired(), Length(min=1, max=64)])
    email = StringField(&amp;#39;Email&amp;#39;, validators=[DataRequired(), Email(), Length(max=120)])
    submit = SubmitField(&amp;#39;Add User&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 6: Create a Template for the Web Application&lt;/h4&gt;
&lt;p&gt;Create a template for users to interact with the web app. Add their usernames and emails with Jinja like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;
    &amp;lt;title&amp;gt;User Management&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css&amp;quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class=&amp;quot;container mt-4&amp;quot;&amp;gt;
        &amp;lt;h1 class=&amp;quot;mb-4&amp;quot;&amp;gt;User Management&amp;lt;/h1&amp;gt;

        &amp;lt;!-- Flash messages --&amp;gt;
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                &amp;lt;div class=&amp;quot;alert alert-{{ messages[0][0] }} alert-dismissible fade show&amp;quot; role=&amp;quot;alert&amp;quot;&amp;gt;
                    {{ messages[0][1] }}
                    &amp;lt;button type=&amp;quot;button&amp;quot; class=&amp;quot;close&amp;quot; data-dismiss=&amp;quot;alert&amp;quot; aria-label=&amp;quot;Close&amp;quot;&amp;gt;
                        &amp;lt;span aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;amp;times;&amp;lt;/span&amp;gt;
                    &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
            {% endif %}
        {% endwith %}

        &amp;lt;!-- User Form --&amp;gt;
        &amp;lt;div class=&amp;quot;card mb-4&amp;quot;&amp;gt;
            &amp;lt;div class=&amp;quot;card-body&amp;quot;&amp;gt;
                &amp;lt;form method=&amp;quot;POST&amp;quot;&amp;gt;
                    {{ form.hidden_tag() }}
                    &amp;lt;div class=&amp;quot;form-group&amp;quot;&amp;gt;
                        {{ form.username.label(class=&amp;quot;form-label&amp;quot;) }}
                        {{ form.username(class=&amp;quot;form-control&amp;quot;) }}
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&amp;quot;form-group&amp;quot;&amp;gt;
                        {{ form.email.label(class=&amp;quot;form-label&amp;quot;) }}
                        {{ form.email(class=&amp;quot;form-control&amp;quot;) }}
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&amp;quot;form-group&amp;quot;&amp;gt;
                        {{ form.submit(class=&amp;quot;btn btn-primary&amp;quot;) }}
                    &amp;lt;/div&amp;gt;
                &amp;lt;/form&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- User List --&amp;gt;
        &amp;lt;h2 class=&amp;quot;mb-3&amp;quot;&amp;gt;List of Users&amp;lt;/h2&amp;gt;
        &amp;lt;table class=&amp;quot;table table-striped&amp;quot;&amp;gt;
            &amp;lt;thead&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;th&amp;gt;ID&amp;lt;/th&amp;gt;
                    &amp;lt;th&amp;gt;Username&amp;lt;/th&amp;gt;
                    &amp;lt;th&amp;gt;Email&amp;lt;/th&amp;gt;
                &amp;lt;/tr&amp;gt;
            &amp;lt;/thead&amp;gt;
            &amp;lt;tbody&amp;gt;
                {% for user in users %}
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;{{ user.id }}&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;{{ user.username }}&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;{{ user.email }}&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
                {% endfor %}
            &amp;lt;/tbody&amp;gt;
        &amp;lt;/table&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script src=&amp;quot;https://code.jquery.com/jquery-3.5.1.slim.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.3/dist/umd/popper.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&amp;quot;https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 7: Run the Application&lt;/h4&gt;
&lt;p&gt;Finally, create the &lt;code&gt;run.py&lt;/code&gt; file at the root of the project to run the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from app import create_app

app = create_app()

if __name__ == &amp;#39;__main__&amp;#39;:
    app.run(debug=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the app, type the following via CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ python3 run.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&amp;#39;s the expected result:
&lt;img src=&quot;/images/blog/2025-02/add-user.png&quot; alt=&quot;Adding users in a Flask application with FlaskSQL-Alchemy&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we&amp;#39;ve presented SQLAlchemy as a powerful tool to manage databases using Python.&lt;/p&gt;
&lt;p&gt;Specifically, using Flask-SQLAlchemy makes it easier to manage databases as this extension leverages SQLAlchemy specifically for Flask apps.&lt;/p&gt;
&lt;p&gt;The example we&amp;#39;ve run through shows how easy it is to use this library.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Integrate AppSignal with AWS Fargate in Python Flask</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/02/12/integrate-appsignal-with-aws-fargate-in-python-flask.html"/>
    <id>https://blog.appsignal.com/2025/02/12/integrate-appsignal-with-aws-fargate-in-python-flask.html</id>
    <published>2025-02-12T00:00:00+00:00</published>
    <updated>2025-02-12T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s integrate AppSignal with a Flask application running on AWS Fargate.</summary>
    <content type="html">&lt;p&gt;In this tutorial, we’ll show you how to integrate AppSignal with a Flask application running on AWS Fargate.&lt;/p&gt;
&lt;p&gt;Fargate is a serverless container service that allows you to run Docker containers in the cloud. By integrating AppSignal with AWS Fargate, you can monitor the performance of your Flask application and get insights.&lt;/p&gt;
&lt;h2&gt;Set Up&lt;/h2&gt;
&lt;p&gt;The Flask application we&amp;#39;ll use in this tutorial is a simple weather API that returns random weather data. We’ll build the cloud infrastructure using the AWS Cloud Development Kit (CDK) and deploy the Flask application to AWS Fargate. The app will run from a Docker image hosted on Amazon Elastic Container Registry (ECR).&lt;/p&gt;
&lt;p&gt;Once the CDK stack is deployed, we’ll monitor the Flask application using AppSignal. The Open Telemetry instrumentation will have a custom span we can use to track the performance of the randomly generated weather data.&lt;/p&gt;
&lt;p&gt;You can follow along with the code presented here or by cloning the &lt;a href=&quot;https://github.com/beautifulcoder/flask-fargate-api&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;PIP packages&lt;/h2&gt;
&lt;p&gt;The weather API will have the following dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;appsignal&lt;/code&gt;: The AppSignal Python agent.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opentelemetry-instrumentation-flask&lt;/code&gt;: The OpenTelemetry instrumentation for Flask.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Flask&lt;/code&gt;: The Flask web framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These requirements can be installed via the &lt;code&gt;requirements.txt&lt;/code&gt; file inside the docker image. To keep it simple, we will use the &lt;code&gt;python:3.12-slim&lt;/code&gt; image as the base image for the Flask application. The docker container can run on any computer that has Docker installed, and it can be deployed to AWS Fargate.&lt;/p&gt;
&lt;p&gt;But first, spin up the CDK project using the Python template. The infrastructure code will be the one to build the docker image inside an &lt;code&gt;app&lt;/code&gt; folder.&lt;/p&gt;
&lt;h2&gt;AWS CDK&lt;/h2&gt;
&lt;p&gt;Be sure to have the &lt;a href=&quot;https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-python.html&quot;&gt;AWS CDK installed&lt;/a&gt; on your computer. We will assume you have Python 3.6 or later installed.&lt;/p&gt;
&lt;p&gt;Create a new project folder and initialize the CDK project using the Python template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir flask-fargate-api
cd flask-fargate-api
cdk init app --language python
./venv/Scripts/activate
pip install -r requirements.txt
cdk bootstrap
cdk synth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;activate&lt;/code&gt; script will activate the virtual environment for your project. The &lt;code&gt;cdk synth&lt;/code&gt; command will verify the CDK project is working correctly.&lt;/p&gt;
&lt;p&gt;Open the &lt;code&gt;app.py&lt;/code&gt; file in the &lt;code&gt;flask-fargate-api&lt;/code&gt; folder and make sure your account details are correct.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;FlaskFargateApiStack(
    app,
    &amp;quot;flask-fargate-api-stack&amp;quot;,
    env=cdk.Environment(account=&amp;#39;1234567890&amp;#39;, region=&amp;#39;us-east-1&amp;#39;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The stack ID &lt;code&gt;flask-fargate-api-stack&lt;/code&gt; will be the name of the stack used by CloudFormation. You can change the name to something else if you like. But do not change the stack ID once the stack has been deployed because it will bomb.&lt;/p&gt;
&lt;p&gt;Open the &lt;code&gt;flask-fargate-api-stack.py&lt;/code&gt; file and add the following code to create the VPC, ECS cluster, and Fargate service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from aws_cdk import (
    Stack,
    aws_ec2 as ec2,
    aws_ecs as ecs,
    aws_ecs_patterns as ecs_patterns,
    aws_logs as logs
)
from constructs import Construct


class FlaskFargateApiStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -&amp;gt; None:
        super().__init__(scope, construct_id, **kwargs)

        vpc = ec2.Vpc(
            self,
            &amp;quot;vpc&amp;quot;
        )

        cluster = ecs.Cluster(
            self,
            &amp;quot;cluster&amp;quot;,
            vpc=vpc
        )

        service = ecs_patterns.ApplicationLoadBalancedFargateService(
            self,
            &amp;quot;api&amp;quot;,
            cluster=cluster,
            cpu=256,
            memory_limit_mib=512,
            desired_count=1,
            public_load_balancer=True,

            task_image_options={
                &amp;quot;environment&amp;quot;: {
                    &amp;quot;ADDRESS&amp;quot;: &amp;quot;0.0.0.0&amp;quot;,
                    &amp;quot;PORT&amp;quot;: &amp;quot;80&amp;quot;
                },
                &amp;quot;image&amp;quot;: ecs.ContainerImage.from_asset(
                    directory=&amp;quot;app&amp;quot;
                ),
                &amp;quot;container_port&amp;quot;: 80,
                &amp;quot;log_driver&amp;quot;: ecs.AwsLogDriver(
                    stream_prefix=&amp;quot;api&amp;quot;,
                    log_retention=logs.RetentionDays.ONE_DAY
                )
            }
        )

        service.target_group.configure_health_check(
            path=&amp;quot;/health&amp;quot;
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The task image has environment variables for the address and port. This will allow the loopback address to be used inside the container. The &lt;code&gt;public_load_balancer&lt;/code&gt; will give us a public URL to access the Flask application.&lt;/p&gt;
&lt;p&gt;The VPC allows the CDK to grant network access to the Fargate service. This allows access to the ECR repository and the ECS cluster. The app itself doesn’t exist yet, but the CDK will build the Docker image and push it to ECR. The health check is used to monitor the health of the Fargate service during deployment.&lt;/p&gt;
&lt;p&gt;We are not able to deploy the stack yet, because it needs the Docker image to be built and pushed to ECR. Let&amp;#39;s do that next.&lt;/p&gt;
&lt;h2&gt;Weather API&lt;/h2&gt;
&lt;p&gt;Create the missing &lt;code&gt;app&lt;/code&gt; folder in the root of the project. Inside the &lt;code&gt;app&lt;/code&gt; folder, create a new file called &lt;code&gt;app.py&lt;/code&gt; and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import appsignal
from appsignal import set_category
from opentelemetry import trace

# open telemetry instrumentation
appsignal.start()
tracer = trace.get_tracer(__name__)

from flask import Flask, jsonify
import random
import os

app = Flask(__name__)

weather_data = [
    {&amp;quot;city&amp;quot;: &amp;quot;New York&amp;quot;, &amp;quot;temperature&amp;quot;: 77, &amp;quot;condition&amp;quot;: &amp;quot;Cloudy&amp;quot;},
    {&amp;quot;city&amp;quot;: &amp;quot;Los Angeles&amp;quot;, &amp;quot;temperature&amp;quot;: 70, &amp;quot;condition&amp;quot;: &amp;quot;Sunny&amp;quot;},
    {&amp;quot;city&amp;quot;: &amp;quot;Chicago&amp;quot;, &amp;quot;temperature&amp;quot;: 73, &amp;quot;condition&amp;quot;: &amp;quot;Sunny&amp;quot;},
    {&amp;quot;city&amp;quot;: &amp;quot;Houston&amp;quot;, &amp;quot;temperature&amp;quot;: 81, &amp;quot;condition&amp;quot;: &amp;quot;Cloudy&amp;quot;},
    {&amp;quot;city&amp;quot;: &amp;quot;Phoenix&amp;quot;, &amp;quot;temperature&amp;quot;: 92, &amp;quot;condition&amp;quot;: &amp;quot;Sunny&amp;quot;},
]


@app.route(&amp;#39;/weather&amp;#39;, methods=[&amp;#39;GET&amp;#39;])
def get_weather():
    # start a custom span
    with tracer.start_as_current_span(&amp;quot;random_weather&amp;quot;):
        set_category(&amp;quot;get_weather&amp;quot;)
        return jsonify(random.choice(weather_data))


@app.route(&amp;#39;/health&amp;#39;, methods=[&amp;#39;GET&amp;#39;])
def health():
    return jsonify({&amp;quot;status&amp;quot;: &amp;quot;ok&amp;quot;})


if __name__ == &amp;#39;__main__&amp;#39;:
    port = os.getenv(&amp;#39;PORT&amp;#39;, 5000)
    host = os.getenv(&amp;#39;ADDRESS&amp;#39;, &amp;#39;localhost&amp;#39;)

    app.run(debug=True, host=host, port=port)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;tracer&lt;/code&gt; object creates a custom span called &lt;code&gt;random_weather&lt;/code&gt;. This span will track the performance of the random weather data. Keep in mind this could be an API call, a database query, or any other operation that needs to be monitored. The &lt;code&gt;set_category&lt;/code&gt; function sets the category of the span to &lt;code&gt;get_weather&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;appsignal.start()&lt;/code&gt; function must be called before the Flask application is imported. This will start the AppSignal agent and configure the OpenTelemetry instrumentation for Flask.&lt;/p&gt;
&lt;p&gt;To activate this instrumentation, create an &lt;code&gt;__appsignal.py__&lt;/code&gt; file in the &lt;code&gt;app&lt;/code&gt; folder and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name=&amp;quot;flask-fargate-api&amp;quot;,
    # Please do not commit this key to your source control management system.
    # Move this to your app&amp;#39;s security credentials or environment variables.
    # https://docs.appsignal.com/python/configuration/options.html#option-push_api_key
    push_api_key=&amp;quot;your-push-api-key&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create the &lt;code&gt;requirements.txt&lt;/code&gt; file in the &lt;code&gt;app&lt;/code&gt; folder and add the following dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appsignal
opentelemetry-instrumentation-flask
Flask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the &lt;code&gt;Dockerfile&lt;/code&gt; in the &lt;code&gt;app&lt;/code&gt; folder and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM python:3.12-slim

WORKDIR /app

COPY . /app

RUN pip install --no-cache-dir -r requirements.txt

CMD [&amp;quot;python&amp;quot;, &amp;quot;app.py&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;Dockerfile&lt;/code&gt; will create the Docker image for the Flask application to run on AWS Fargate. The command &lt;code&gt;python app.py&lt;/code&gt; will run the Flask application when the container is started. You can test the Flask application locally or via the Docker container.&lt;/p&gt;
&lt;p&gt;That’s it for the Flask application. The next step is to build the Docker image and push it to ECR via the CDK.&lt;/p&gt;
&lt;h3&gt;Deploy the CDK Stack&lt;/h3&gt;
&lt;p&gt;Drum roll, please! Fire up the CDK stack using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cdk deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If there is an issue with the Docker image, be sure to have Docker installed and running on your computer. Docker must also have access to ECR to push the image.&lt;/p&gt;
&lt;p&gt;If it’s having trouble signing in to ECR, run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 1234567890.dkr.ecr.us-east-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be sure to replace &lt;code&gt;1234567890&lt;/code&gt; with your AWS account number. This will allow Docker to push the image to ECR.&lt;/p&gt;
&lt;p&gt;The deployment should take no more than five minutes. If it is taking longer, kill it in CloudFormation and delete the stack. Troubleshoot the issue and try again. AWS charges for the Fargate service and ECR, so do not leave the stack running if it is not working.&lt;/p&gt;
&lt;p&gt;At the end, the CDK will output the public URL for the Flask application. Look for the &lt;code&gt;api&lt;/code&gt; URL in the output, which looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Outputs:
flask-fargate-api-stack.api = http://api-1234567890.us-east-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now grab the URL and test the Flask application via CURL or a web browser. The &lt;code&gt;/weather&lt;/code&gt; endpoint should return random weather data. The &lt;code&gt;/health&lt;/code&gt; endpoint should return a status of &lt;code&gt;ok&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Hit the &lt;code&gt;/weather&lt;/code&gt; endpoint a few times to generate some data. Next, we will monitor the Flask application using AppSignal.&lt;/p&gt;
&lt;h2&gt;AppSignal Monitoring of the Python Flask Application&lt;/h2&gt;
&lt;p&gt;Once you hit the endpoint enough times, go to the AppSignal dashboard under your account. The application should be listed under &lt;code&gt;flask-fargate-api&lt;/code&gt;. The name matches the name configuration in the &lt;code&gt;__appsignal__.py&lt;/code&gt; file. Click on the application to view the dashboard.&lt;/p&gt;
&lt;p&gt;Under Performance, you should see Actions, Slow Events, and Graphs. Click on Actions and then the &lt;code&gt;GET /health&lt;/code&gt; endpoint. Then, you can click on &amp;#39;Slow Events&amp;#39; to see your custom span &lt;code&gt;random_weather&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;random_weather&lt;/code&gt; span should show average duration, throughput, and overall performance impact. This is the custom span we created to monitor the performance of the random weather data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-02/flask-fargate-api-1.png&quot; alt=&quot;AppSignal Dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Note the span breakdown, which includes impact, meaning the endpoint spent all its time generating random weather data.&lt;/p&gt;
&lt;p&gt;You may also track the performance of each call for the custom span.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-02/flask-fargate-api-2.png&quot; alt=&quot;AppSignal Custom Span&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The breakdown of the span itself is also available. This shows a timeline of the HTTP request and the time spent generating the random weather data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-02/flask-fargate-api-3.png&quot; alt=&quot;AppSignal Span Breakdown&quot;/&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s that!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this tutorial, we showed you how to integrate AppSignal with a Flask application running on AWS Fargate. We built the cloud infrastructure using the AWS CDK and deployed the application. We then monitored the Flask application using AppSignal and created a custom span to track the performance of the random weather data.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Monitor the Performance of Your Python Flask Application with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/01/29/monitor-the-performance-of-your-python-flask-application-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2025/01/29/monitor-the-performance-of-your-python-flask-application-with-appsignal.html</id>
    <published>2025-01-29T00:00:00+00:00</published>
    <updated>2025-01-29T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s use AppSignal to monitor and improve the performance of your Flask applications.</summary>
    <content type="html">&lt;p&gt;When a system runs slowly, our first thought might be that it’s failing. This common reaction underscores a key point:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Performance is crucial for an application&amp;#39;s readiness and maturity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the world of web applications, even milliseconds matter. Performance impacts user satisfaction and operational efficiency, making it a critical factor.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll show you how to use AppSignal to monitor and improve the performance of your Flask applications.&lt;/p&gt;
&lt;h2&gt;Flask Performance Monitoring Essentials: Key Metrics&lt;/h2&gt;
&lt;p&gt;Achieving optimal performance in Flask applications involves a comprehensive approach. This means building applications that not only run efficiently but also maintain this efficiency as they scale. Key metrics provide the data needed to guide our optimization efforts. Let’s explore some of these essential metrics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Response Time&lt;/strong&gt;: This is a direct indicator of user experience, measuring the time taken for a user&amp;#39;s request to be processed and the response to be sent back. In a Flask application, factors such as database queries, view processing, and middleware operations can influence response times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput&lt;/strong&gt;: Throughput refers to the number of requests your application can handle within a given timeframe. Higher throughput indicates better performance under load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Rates&lt;/strong&gt;: The frequency of errors (4xx and 5xx HTTP responses) can indicate issues with code, database queries, or server configuration. Monitoring error rates helps to quickly identify and fix problems that could degrade user experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database Performance Metrics&lt;/strong&gt;: These include the number of queries per request, query execution time, and the efficiency of database connections. Optimizing these metrics can significantly improve overall application performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handling Concurrent Users&lt;/strong&gt;: The ability to efficiently serve multiple users accessing your Flask application simultaneously is critical. Monitoring how well your application handles concurrent users helps ensure it can scale effectively without delays.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What We Will Build&lt;/h2&gt;
&lt;p&gt;In this article, we&amp;#39;ll construct a Flask-based blog application ready for high-traffic events, integrating AppSignal to monitor, optimize, and ensure it scales seamlessly under load.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/amirtds/myblog&quot;&gt;Here&amp;#39;s the codebase&lt;/a&gt; and the branch &lt;code&gt;appsignal-integration&lt;/code&gt; is where we have AppSignal integrated with our Flask app. You can &lt;a href=&quot;https://myblog-6lnz.onrender.com/&quot;&gt;access the live site&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;To follow along, you&amp;#39;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.12.4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flask.palletsprojects.com/en/3.0.x/changes/#version-3-0-3&quot;&gt;Flask 3.0.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/support/operating-systems.html&quot;&gt;An AppSignal-supported operating system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://appsignal.com/accounts&quot;&gt;An AppSignal account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flask.palletsprojects.com/en/3.0.x/&quot;&gt;Basic Flask knowledge&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Prepare the Project&lt;/h3&gt;
&lt;p&gt;Now let&amp;#39;s create a directory for our project, clone it from GitHub, and install all the requirements:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;mkdir flask-performance &amp;amp;&amp;amp; cd flask-performance
python3.12 -m venv venv
source venv/bin/activate
git clone -b main https://github.com/amirtds/myblog
cd myblog
python3.12 -m pip install -r requirements.txt
npm run build:css
python run.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now visit &lt;a href=&quot;http://127.0.0.1:8000&quot;&gt;127.0.0.1:8000&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Install AppSignal&lt;/h3&gt;
&lt;p&gt;Now we are going to install &lt;a href=&quot;https://pypi.org/project/appsignal/&quot;&gt;appsignal&lt;/a&gt; and &lt;a href=&quot;https://pypi.org/project/opentelemetry-instrumentation-flask/&quot;&gt;opentelemetry-instrumentation-flask&lt;/a&gt; in our project.&lt;/p&gt;
&lt;p&gt;Before installing these packages, &lt;a href=&quot;https://appsignal.com/users/sign_in&quot;&gt;log in&lt;/a&gt; to AppSignal using your credentials. After picking the organization, click on &lt;strong&gt;Add app&lt;/strong&gt; on the top right of the navigation bar. Select Python as the language and note the &lt;strong&gt;push-api-key&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Make sure your virtual environment is activated and run the following commands in the project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3.12 -m pip install appsignal==1.3.6
python3.12 -m appsignal install --push-api-key [YOU-KEY]
python3.12 -m pip install opentelemetry-instrumentation-flask==0.46b0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Provide the app name to the CLI prompt. After finishing the installation, you should see a new file called &lt;code&gt;__appsignal__.py&lt;/code&gt; in your project.&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s create an .env file in the project root and add &lt;code&gt;APPSIGNAL_PUSH_API_KEY=YOUR-KEY&lt;/code&gt; (remember to change the value to your actual key). Next, let&amp;#39;s change the content of the &lt;code&gt;__appsignal__.py&lt;/code&gt; file to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# __appsignal__.py
import os
from appsignal import Appsignal

# Load environment variables from the .env file
from dotenv import load_dotenv
load_dotenv()

# Get APPSIGNAL_PUSH_API_KEY from environment
push_api_key = os.getenv(&amp;#39;APPSIGNAL_PUSH_API_KEY&amp;#39;)

appsignal = Appsignal(
    active=True,
    name=&amp;quot;myblog&amp;quot;,
    push_api_key=os.getenv(&amp;quot;APPSIGNAL_PUSH_API_KEY&amp;quot;),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And update the &lt;code&gt;__init__.py&lt;/code&gt; to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;from flask import Flask
from __appsignal__ import appsignal # new line
from opentelemetry.instrumentation.flask import FlaskInstrumentor # new line
from opentelemetry import trace # new line

def create_app():
    appsignal.start() # new line
    app = Flask(__name__)
    app.static_folder = &amp;#39;static&amp;#39;

    # Instrument Flask with OpenTelemetry
    FlaskInstrumentor().instrument_app(app) # new line

    from . import routes
    app.register_blueprint(routes.bp)

    return app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have imported &lt;strong&gt;appsignal&lt;/strong&gt; and started it using the configuration we used in &lt;strong&gt;__appsignal__.py&lt;/strong&gt;.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h2&gt;Monitor Concurrent Users&lt;/h2&gt;
&lt;p&gt;Now let&amp;#39;s look at monitoring concurrent users.&lt;/p&gt;
&lt;h3&gt;Scenario: Optimizing for High Load on Blog Posts&lt;/h3&gt;
&lt;p&gt;As we anticipate increased traffic on our Flask-based blog application, we want to ensure our site remains fast and responsive. We&amp;#39;ve had traffic spikes that led to slow load times and occasional downtime. We&amp;#39;re determined to avoid these issues by thoroughly testing and optimizing our site beforehand. We&amp;#39;ll use Locust to simulate user traffic and AppSignal to monitor our application&amp;#39;s performance.&lt;/p&gt;
&lt;h3&gt;Creating a Locust Test for Simulated Traffic&lt;/h3&gt;
&lt;p&gt;First, we&amp;#39;ll create a &lt;code&gt;locustfile.py&lt;/code&gt; that simulates users navigating through critical parts of our blog: the homepage and individual post pages. This simulation helps us understand how our site performs under pressure.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;locustfile.py&lt;/code&gt; in the project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# locustfile.py
from locust import HttpUser, between, task
import random

class BlogUser(HttpUser):
    wait_time = between(1, 3)  # Users wait 1-3 seconds between tasks
    post_urls = [&amp;quot;/post/post1&amp;quot;, &amp;quot;/post/post2&amp;quot;, &amp;quot;/post/post3&amp;quot;, &amp;quot;/post/post4&amp;quot;, &amp;quot;/post/post5&amp;quot;, &amp;quot;/post/post6&amp;quot;]

    @task
    def index_page(self):
        self.client.get(&amp;quot;/&amp;quot;)

    @task(3)
    def view_post(self):
        post_url = random.choice(self.post_urls)
        self.client.get(post_url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this &lt;code&gt;locustfile.py&lt;/code&gt;, users primarily visit the post pages, occasionally returning to the homepage. This pattern mimics realistic user behavior on a blog.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Before running Locust, ensure you have posts available in your Flask app. You can check this by visiting the homepage and individual post pages.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Defining Acceptable Response Times&lt;/h3&gt;
&lt;p&gt;Before we start, let&amp;#39;s define what we consider acceptable response times. For a smooth user experience, we aim for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Homepage: &lt;strong&gt;under 1 second&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Post page: &lt;strong&gt;under 1.5 seconds&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These targets ensure users experience minimal delay, keeping their engagement high.&lt;/p&gt;
&lt;h3&gt;Conducting the Test and Monitoring Results&lt;/h3&gt;
&lt;p&gt;With our Locust test ready, we run it to simulate the 500 users and observe results in real time. Here&amp;#39;s how:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start the Locust test by running &lt;code&gt;locust -f locustfile.py&lt;/code&gt; in your terminal, then open &lt;a href=&quot;http://localhost:8089&quot;&gt;http://localhost:8089&lt;/a&gt; to set up and start the simulation. Set the &lt;strong&gt;Number of Users&lt;/strong&gt; to 500 and set the host to &lt;a href=&quot;http://127.0.0.1:8000&quot;&gt;http://127.0.0.1:8000&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Monitor performance in both Locust&amp;#39;s web interface and AppSignal. Locust shows us request rates and response times, while AppSignal provides deeper insights into our Flask app&amp;#39;s behavior under load.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After running Locust, you can find information about the load test in its dashboard:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/locust_dashboard.png&quot; alt=&quot;Locust dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Now go to your application page in AppSignal. Under the &lt;strong&gt;Performance&lt;/strong&gt; section, click on Actions and you should see something similar to the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/performance_lists.png&quot; alt=&quot;AppSignal action monitoring list&quot;/&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mean:&lt;/strong&gt; This is the average response time for all the requests made to a particular endpoint. It provides a general idea of how long it takes for the server to respond. In our context, any mean response time greater than 1 second could be considered a red flag, indicating that our application&amp;#39;s performance might not meet user expectations for speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90th Percentile:&lt;/strong&gt; This is the response time at the 90th percentile. For example, for &lt;code&gt;GET /&lt;/code&gt;, if we have 7 ms, it means 90% of requests are completed in 7 ms or less.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput:&lt;/strong&gt; This is the number of requests handled per second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now let&amp;#39;s click on the &lt;strong&gt;Graphs&lt;/strong&gt; option under &lt;strong&gt;Performance&lt;/strong&gt;. You should see something like the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/performance_graph.png&quot; alt=&quot;AppSignal performance graphs&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Analyzing and Improving Performance&lt;/h3&gt;
&lt;p&gt;We need to prepare our site for increased traffic as response times might exceed our targets. Here&amp;#39;s a simplified plan:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Database queries:&lt;/strong&gt; Slow queries often cause performance issues. We will delve into this in the next section.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static assets:&lt;/strong&gt; Ensure they&amp;#39;re properly cached. Use a CDN for better delivery speeds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application resources:&lt;/strong&gt; Sometimes, the solution is as straightforward as adding more RAM and CPUs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Track Errors&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s introduce an error in one of the routes. For example, we can create a route that divides by zero, which will cause a &lt;code&gt;ZeroDivisionError&lt;/code&gt;.
In the end of the &lt;code&gt;routes.py&lt;/code&gt; file, let&amp;#39;s add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# Introduce a route that causes an error
@bp.route(&amp;#39;/cause-error&amp;#39;)
def cause_error():
    # This will cause a ZeroDivisionError
    1 / 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now visit &lt;a href=&quot;http://127.0.0.1:8000/cause-error&quot;&gt;http://127.0.0.1:8000/cause-error&lt;/a&gt;. We should see a &lt;code&gt;ZeroDivisionError&lt;/code&gt; in the dashboard.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s head to the dashboard in AppSignal. On the left sidebar, click on &lt;strong&gt;Errors -&amp;gt; Issue List&lt;/strong&gt;. Here you should see all the errors that occur to users when they visit specific URLs. By clicking on each issue, you should see more detailed information.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/appsignal_error_dashboard.png&quot; alt=&quot;AppSignal error dashboard&quot;/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is highly recommended that you integrate AppSignal with your GitHub repo. When an error occurs, an issue will be created in the repo automatically so you don&amp;#39;t lose track of it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No additional steps are required beyond the initial setup to begin logging and managing errors.&lt;/p&gt;
&lt;p&gt;In the Graphs we can see the Error rate, Count, and Throughput:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/appsignal_error_graph.png&quot; alt=&quot;AppSignal error graph&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Monitor Performance&lt;/h2&gt;
&lt;p&gt;In the &lt;strong&gt;Performance&lt;/strong&gt; section of your dashboard, click on the &lt;strong&gt;Issues&lt;/strong&gt; list to see a detailed breakdown of all visited URLs along with key metrics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mean:&lt;/strong&gt; This metric indicates the average response time for requests to a specific endpoint. It gives us a sense of the overall speed of our server&amp;#39;s responses. For our blog, if the mean response time exceeds 1 second, it could be a warning sign that our performance may not meet user expectations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput:&lt;/strong&gt; This metric shows the number of requests being processed per second. High throughput with acceptable response times indicates good performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Impact:&lt;/strong&gt; This reflects the significance of a particular action on the overall application performance, based on its usage relative to other actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/performance_lists.png&quot; alt=&quot;Performance issues list&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Additionally, we can visualize our application&amp;#39;s performance through various graphs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/performance_graph.png&quot; alt=&quot;Performance graphs&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This data is crucial as it provides insights into how users experience page load times. High response times signal the need for optimization. For instance, if multiple requests exceed 1 second, we should investigate and improve those areas to enhance performance.&lt;/p&gt;
&lt;p&gt;By using these insights from AppSignal, we can continually refine and optimize our Flask application to ensure it delivers a fast and smooth user experience.&lt;/p&gt;
&lt;h2&gt;Anomaly Detection and Alerts&lt;/h2&gt;
&lt;p&gt;We can also set up triggers to alert us to high resource usage on our platform. This is crucial for taking action before our site goes down due to a spike in traffic. Additionally, this is important because when a critical function fails, we may see a surge in errors. Anomaly detection can notify us before this impacts more users.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s create two triggers: one for &lt;strong&gt;high memory usage&lt;/strong&gt; when it exceeds &lt;em&gt;70%&lt;/em&gt; and another for &lt;strong&gt;error rates&lt;/strong&gt; when they surpass &lt;em&gt;20%&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/anomaly_trigger_1.png&quot; alt=&quot;Anomaly trigger 1&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/anomaly_trigger_2.png&quot; alt=&quot;Anomaly trigger 2&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/anomaly_triggers.png&quot; alt=&quot;Active anomaly triggers&quot;/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ensure the Email notification option is checked to receive notifications when an anomaly occurs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the &lt;strong&gt;Issues&lt;/strong&gt; list, you can view all the anomalies that have occurred based on the conditions you defined.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/anomaly_happened.png&quot; alt=&quot;Detected anomalies list&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Uptime Monitoring&lt;/h2&gt;
&lt;p&gt;Uptime monitoring is essential to ensure your blog remains accessible and performs well. Here&amp;#39;s why you might need uptime monitoring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Post-Deployment Issues:&lt;/strong&gt; After deploying updates, there might be changes in URLs or resources, such as CSS files, leading to errors and a broken appearance. This not only affects the aesthetics but can also frustrate users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System Downtimes:&lt;/strong&gt; Various issues, like database outages or failures in third-party services, can cause your site to go down. Downtimes disrupt user experience and can damage your platform&amp;#39;s reputation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Uptime monitoring not only confirms that your site is operational but also alerts you immediately if it goes down.&lt;/p&gt;
&lt;h3&gt;Features of Uptime Monitoring&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Regional Response Times:&lt;/strong&gt; AppSignal&amp;#39;s uptime check shows response times from different regions, providing insights into global performance. This is particularly useful for ensuring a consistent user experience across various geographical locations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public Status Page:&lt;/strong&gt; Set up a public status page to communicate real-time site status, reducing the number of support tickets as users can verify the site status themselves.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/public_uptime_check.png&quot; alt=&quot;Public status page&quot;/&gt;&lt;/p&gt;
&lt;p&gt;In your &lt;strong&gt;Uptime Monitoring&lt;/strong&gt; dashboard, you can add new URLs by clicking on &lt;strong&gt;Add Uptime Monitor&lt;/strong&gt;. For instance, you can create monitors for your blog&amp;#39;s critical resources and main URLs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/uptime_1.png&quot; alt=&quot;Uptime monitor&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2025-01/uptime_2.png&quot; alt=&quot;Uptime monitor showing response times by region&quot;/&gt;&lt;/p&gt;
&lt;p&gt;By setting up these alerts and monitors, you ensure that your Flask blog application remains robust and performs well, even under high traffic or unexpected issues.&lt;/p&gt;
&lt;h2&gt;Host Monitoring&lt;/h2&gt;
&lt;p&gt;In this section, we can observe how our VM resources are being utilized by our Flask application. The metrics section in AppSignal provides visualizations and graphs, making it easier to understand resource usage.&lt;/p&gt;
&lt;h3&gt;Monitoring Resource Usage&lt;/h3&gt;
&lt;p&gt;Using AppSignal&amp;#39;s Host Monitoring, we can track various aspects of our VM&amp;#39;s performance. Here are some of the key metrics we can visualize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU Usage:&lt;/strong&gt; This metric shows how much CPU is being used by our application. High CPU usage can indicate that our application is under heavy load or that processes need optimization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Usage:&lt;/strong&gt; Monitoring memory usage helps us understand how much memory our application consumes. If memory usage is consistently high, optimizing the application or increasing the available memory might be necessary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disk I/O:&lt;/strong&gt; Disk I/O metrics show how much data is being read from or written to the disk. High disk I/O can be a sign of heavy data processing tasks or inefficient data handling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/host_metric_1.png&quot; alt=&quot;Host metric 1&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/host_metric_2.png&quot; alt=&quot;Host metric 2&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/host_metric_3.png&quot; alt=&quot;Host metric 3&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Importance of Resource Monitoring&lt;/h3&gt;
&lt;p&gt;Resource monitoring is crucial for maintaining the performance and reliability of our Flask application. By keeping an eye on these metrics, we can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Identify Bottlenecks:&lt;/strong&gt; High resource usage can indicate performance bottlenecks that need to be addressed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimize Performance:&lt;/strong&gt; Understanding resource consumption helps in optimizing the application for better performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prevent Downtime:&lt;/strong&gt; Monitoring helps in proactively managing resources, preventing potential downtimes due to resource exhaustion.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By using these insights from AppSignal, we can ensure that our Flask blog application remains performant and reliable.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Monitoring the performance of your Flask application is crucial for ensuring a smooth and responsive user experience, especially as traffic increases. By integrating AppSignal, we can gain valuable insights into various performance metrics, identify potential bottlenecks, and take proactive measures to optimize our application.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to Use Regular Expressions in Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2025/01/15/how-to-use-regular-expressions-in-python.html"/>
    <id>https://blog.appsignal.com/2025/01/15/how-to-use-regular-expressions-in-python.html</id>
    <published>2025-01-15T00:00:00+00:00</published>
    <updated>2025-01-15T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s explore the basics of regular expressions in Python, as well as more advanced techniques, real-world use cases, and performance optimization strategies.</summary>
    <content type="html">&lt;p&gt;Regular expressions, commonly known as regex, are a tool for text processing and pattern matching.&lt;/p&gt;
&lt;p&gt;In Python, the &lt;code&gt;re&lt;/code&gt; module offers a robust implementation of regex, allowing developers to handle complex text manipulation efficiently.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll get to grips with regular expressions and provide practical code examples — from the basic to more advanced — so you can understand how to use regex in Python.&lt;/p&gt;
&lt;h2&gt;Basics of Regular Expressions&lt;/h2&gt;
&lt;p&gt;Regular expressions are sequences of characters that define search patterns. They can be used for a variety of tasks, such as searching, editing, and manipulating text.&lt;/p&gt;
&lt;p&gt;In this section, let&amp;#39;s explore some basic components of regex.&lt;/p&gt;
&lt;h3&gt;Literals&lt;/h3&gt;
&lt;p&gt;Literals are the simplest form of regex patterns because they match the exact characters in a search string.&lt;/p&gt;
&lt;p&gt;For example, if you want to find the exact word &amp;quot;&lt;em&gt;cat&lt;/em&gt;&amp;quot; in the text &amp;quot;&lt;em&gt;The cat sat on the mat&lt;/em&gt;&amp;quot;, you&amp;#39;ll have to use a literal that matches the pattern &lt;em&gt;cat&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;Metacharacters&lt;/h3&gt;
&lt;p&gt;In regular expressions, metacharacters are symbols with special meanings and purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&gt; matches any character, except on a new line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;^&lt;/code&gt; matches the start of a string.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$&lt;/code&gt; matches the end of a string.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt; matches 0 or more repetitions of the preceding element.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt; matches 1 or more repetitions of the preceding element.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt; matches 0 or 1 repetition of the preceding element.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{}&lt;/code&gt; matches a specific number of repetitions of the preceding element.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Character Classes&lt;/h3&gt;
&lt;p&gt;Character classes match any character inside a given set. Common examples are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[abc]&lt;/code&gt;: matches any of the characters a, b, or c.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\d&lt;/code&gt; matches any digit (equivalent to [0-9]).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\w&lt;/code&gt; matches any word character (alphanumeric plus underscore).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\s&lt;/code&gt; matches any whitespace character.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Quantifiers&lt;/h3&gt;
&lt;p&gt;Quantifiers specify the number of a character&amp;#39;s or group&amp;#39;s occurrences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;: 0 or more.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt;: 1 or more.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt;: 0 or 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{n}&lt;/code&gt;: Exactly n.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{n,}&lt;/code&gt;: n or more.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{n,m}&lt;/code&gt;: Between n and m.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://www3.ntu.edu.sg/home/ehchua/programming/howto/Regexe.html&quot;&gt;Check out other examples with tutorials&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The &lt;code&gt;re&lt;/code&gt; Module in Python&lt;/h2&gt;
&lt;p&gt;To work with regex in Python, we use the module &lt;code&gt;re&lt;/code&gt; (provided by the standard Python library, so you don&amp;#39;t need to install it separately).&lt;/p&gt;
&lt;p&gt;In this section, we&amp;#39;ll provide some basic examples of how to use the module &lt;code&gt;re&lt;/code&gt; and its functionalities.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;re.search()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;re.search()&lt;/code&gt; function searches for the first match of the regex pattern in a string.&lt;/p&gt;
&lt;p&gt;Suppose, for example, that you want to extract any numbers in a string. We could write the following Python code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

# Define the pattern
pattern = r&amp;quot;\d+&amp;quot;
# Define the text to analyz
text = &amp;quot;There are 123 apples.&amp;quot;
# Apply the regex
match = re.search(pattern, text)
print(match.group())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#39;s explain the &lt;code&gt;pattern = r&amp;quot;\d+&amp;quot;&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The prefix &lt;code&gt;r&lt;/code&gt; defines a raw string that treats backslashes (&lt;code&gt;\&lt;/code&gt;) as literal characters and not as escape characters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\d&lt;/code&gt; matches any digit (equivalent to [0-9]).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt; indicates that the previous pattern (the digits) must appear one or more times.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, if the &lt;code&gt;re.search()&lt;/code&gt; function finds a match, the &lt;code&gt;match&lt;/code&gt; variable returns a match object. So, the &lt;code&gt;group()&lt;/code&gt; method of the &lt;code&gt;match&lt;/code&gt; object returns the substring corresponding to the pattern found.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;re.match()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;re.match()&lt;/code&gt; function checks for a match &lt;strong&gt;only&lt;/strong&gt; at the beginning of a string.&lt;/p&gt;
&lt;p&gt;For example, if we want to search for a numerical match at the beginning of the previously defined text string, we could write the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

# Define the pattern
pattern = r&amp;quot;\d+&amp;quot;
# Define the text to analyze
text = &amp;quot;There are 123 apples.&amp;quot;
# Apply the regex
match = re.match(r&amp;quot;\d+&amp;quot;, text)
print(match)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, as we would expect, here&amp;#39;s the result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, there&amp;#39;s no match with the applied regex rules in the provided string.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;re.findall()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;re.findall()&lt;/code&gt; function finds all matches of the regex pattern in a string, returning the output in a list.&lt;/p&gt;
&lt;p&gt;For example, if you want to find all the numerical patterns in a string, you could write something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re
pattern = r&amp;quot;\d+&amp;quot;
text = &amp;quot;There are 123 apples in 2 trees.&amp;quot;
matches = re.findall(pattern, text)
print(matches)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the result is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[&amp;#39;123&amp;#39;, &amp;#39;2&amp;#39;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, two numerical expressions are found in the string.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;re.sub()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;re.sub()&lt;/code&gt; function replaces a matched regex pattern with a replacement string.&lt;/p&gt;
&lt;p&gt;For example, if you want to replace &amp;quot;123&amp;quot; with &amp;quot;many&amp;quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

# Define the pattern
pattern = r&amp;quot;\d+&amp;quot;
# Define the text to analyze
text = &amp;quot;There are 123 apples.&amp;quot;
# Apply the regex
replaced_text = re.sub(r&amp;quot;\d+&amp;quot;, &amp;quot;many&amp;quot;, text)
print(replaced_text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;There are many apples.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the &lt;code&gt;re.sub()&lt;/code&gt; function applies to all the matches it finds. So, for example, the following code sample:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

# Define the pattern
pattern = r&amp;quot;\d+&amp;quot;
# Define the text to analyze
text = &amp;quot;There are 123 apples in 3 trees.&amp;quot;
# Apply the regex
replaced_text = re.sub(r&amp;quot;\d+&amp;quot;, &amp;quot;many&amp;quot;, text)
print(replaced_text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;There are many apples in many trees.
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h3&gt;When To Use Each Function&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;re.match()&lt;/code&gt;, &lt;code&gt;re.search()&lt;/code&gt;. and &lt;code&gt;re.findall()&lt;/code&gt; are similar. So when should we use one over the other?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;re.search()&lt;/code&gt; searches for the first place where the pattern matches a string. Use it when you need to find the first occurrence of a pattern in a string, regardless of where it is.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;re.match()&lt;/code&gt; searches for a match only at the beginning of a string. Use it to check if a string starts with a certain pattern.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;re.findall()&lt;/code&gt; finds all matches of a pattern in a string and returns them as a list of strings. Use it when you need to find all pattern occurrences in a string.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Advanced Regex Techniques for Python&lt;/h2&gt;
&lt;p&gt;In this section, we&amp;#39;ll dive into some more advanced regex techniques for Python.&lt;/p&gt;
&lt;h3&gt;Lookaheads and Lookbehinds&lt;/h3&gt;
&lt;p&gt;Lookaheads and lookbehinds are types of zero-width assertions that match a position in a string based on what precedes or follows it (without including the preceding or following elements in the match itself).&lt;/p&gt;
&lt;p&gt;A lookahead asserts that a certain pattern must follow the current position in the string but does not include this pattern in the matched result.&lt;/p&gt;
&lt;p&gt;A lookbehind, on the other hand, asserts that a certain pattern must precede the current position in the string, but does not include this pattern in the matched result.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s consider a lookahead example. We want to match sequences of one or more word characters immediately followed by a period:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

pattern = r&amp;quot;\w+(?=\.)&amp;quot;
text = &amp;quot;This is a test. Followed by another test.&amp;quot;
matches = re.findall(pattern, text)
print(matches)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[&amp;#39;test&amp;#39;, &amp;#39;test&amp;#39;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why is this the result? Because the word &amp;quot;test&amp;quot; is followed by a dot, and the sequence is matched two times in the &lt;code&gt;text&lt;/code&gt; variable. We use the &lt;code&gt;r&amp;quot;\w+(?=\.)&amp;quot;&lt;/code&gt; pattern, because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\w+&lt;/code&gt; matches one or more word characters (letters, digits, and underscores).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(?=\.)&lt;/code&gt; is a positive lookahead asserting that the word characters must be followed by a period. However, the period itself is not included in the match result because lookaheads are zero-width assertions.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: A positive lookahead (syntax: &lt;code&gt;(?=...)&lt;/code&gt;) asserts that what follows the current position matches the specified pattern, while a negative lookahead (syntax: &lt;code&gt;(?!...)&lt;/code&gt;) states that what follows the current position does not match the specified pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, let&amp;#39;s consider a lookbehind example. We want to match one or more word characters immediately preceded by a whitespace character. So, consider the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

pattern = r&amp;quot;(?&amp;lt;=\s)\w+&amp;quot;
text = &amp;quot;This is a test. Followed by another test.&amp;quot;
matches = re.findall(pattern, text)
print(matches)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[&amp;#39;is&amp;#39;, &amp;#39;a&amp;#39;, &amp;#39;test&amp;#39;, &amp;#39;Followed&amp;#39;, &amp;#39;by&amp;#39;, &amp;#39;another&amp;#39;, &amp;#39;test&amp;#39;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#39;s explain the code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(?&amp;lt;=\s)&lt;/code&gt; is a positive lookbehind asserting that the current position in the string must be preceded by a whitespace character (&lt;code&gt;\s&lt;/code&gt;), such as a space, tab, or new line. The whitespace itself is not included in the match result because lookbehinds are zero-width assertions.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\w+&lt;/code&gt; matches one or more word characters (letters, digits, and underscores).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, the regex pattern &lt;code&gt;(?&amp;lt;=\s)\w+&lt;/code&gt; looks for sequences of one or more word characters that are immediately preceded by a whitespace character. And since we used the
&lt;code&gt;re.findall()&lt;/code&gt; function, the code prints all the words of the text except for &amp;quot;This&amp;quot; because it is at the beginning of the sentence (so it has no whitespace before it).&lt;/p&gt;
&lt;h3&gt;Non-capturing Groups&lt;/h3&gt;
&lt;p&gt;Non-capturing groups (syntax: &lt;code&gt;(?:...)&lt;/code&gt;) are used to group parts of a pattern without capturing them for later reference. This technique is useful when you need to group parts of your pattern to apply quantifiers, alternation, or other regex operations but do not need to store the match for later use.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s consider a practical example. Suppose you have a sequence of numbers like &lt;code&gt;&amp;quot;123-45-6789&amp;quot;&lt;/code&gt;. You want to intercept the part of the number that has two digits. Here&amp;#39;s how to do so with regex in Python:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

pattern = r&amp;quot;(?:\d{3})-(\d{2})-(\d{4})&amp;quot;
text = &amp;quot;123-45-6789&amp;quot;
match = re.search(pattern, text)
print(match.group(1))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;45
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#39;s explain the code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(?:\d{3})&lt;/code&gt; is a non-capturing group that matches exactly three digits.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;- &lt;/code&gt; matches the hyphen character literally.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(\d{2})&lt;/code&gt; is a capturing group that matches exactly two digits and captures this match for later reference.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(\d{4})&lt;/code&gt; is another capturing group that matches exactly four digits and captures this match for later reference.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;match.group(1)&lt;/code&gt; retrieves the content of the first capturing group from the match object. So, in this case, it prints the two digits captured by the first capturing group &lt;code&gt;(\d{2})&lt;/code&gt;, which is &lt;code&gt;45&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Real-World Examples and Use Cases&lt;/h2&gt;
&lt;p&gt;Now, let&amp;#39;s examine some real-world scenarios to give a more practical idea of how and when to use regex in Python.&lt;/p&gt;
&lt;h3&gt;Data Validation&lt;/h3&gt;
&lt;p&gt;Regular expressions are commonly used to validate data formats such as email addresses, phone numbers, and dates.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pattern = r&amp;quot;^[\w\.-]+@[\w\.-]+\.\w+$&amp;quot;
email = &amp;quot;example@example.com&amp;quot;
match = re.match(pattern, email)
print(bool(match))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, the result of the &lt;code&gt;print()&lt;/code&gt; function is &lt;code&gt;True&lt;/code&gt;, meaning that the string &lt;code&gt;example@example.com&lt;/code&gt; has been recognized as an email address by the regex pattern. Here&amp;#39;s how this works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;^&lt;/code&gt; asserts the position at the start of the string.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[\w\.-]+&lt;/code&gt; matches one or more word characters (alphanumeric characters and underscores), dots, or hyphens.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@&lt;/code&gt; matches the &lt;code&gt;@&lt;/code&gt; character.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\.&lt;/code&gt; matches a literal dot.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\w+&lt;/code&gt; matches one or more word characters (alphanumeric characters and underscores).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$&lt;/code&gt; asserts the position at the end of the string.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So this pattern searches for word characters before and after the &lt;code&gt;@&lt;/code&gt; character. It also searches for a literal dot and other words after it. That&amp;#39;s how email addresses are created.&lt;/p&gt;
&lt;h3&gt;Extracting IP Addresses&lt;/h3&gt;
&lt;p&gt;Another typical use case is extracting an IP address from a log file.&lt;/p&gt;
&lt;p&gt;In Python, we can create it like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

pattern = r&amp;quot;\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b&amp;quot;
log = &amp;quot;User logged in from IP 192.168.0.1&amp;quot;
ip = re.search(pattern, log)
print(ip.group())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#39;s explain the pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\b&lt;/code&gt; asserts a word boundary, ensuring the IP address is not part of a larger string of digits.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\d{1,3}&lt;/code&gt; matches 1 to 3 digits.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\.&lt;/code&gt; matches a literal dot.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Repeated three times, this pattern is the schema of an IP address.&lt;/p&gt;
&lt;h2&gt;Performance Considerations&lt;/h2&gt;
&lt;p&gt;Regular expressions can be computationally expensive, especially with complex patterns.&lt;/p&gt;
&lt;p&gt;So, in this section, we&amp;#39;ll provide some tips for optimizing regex performance.&lt;/p&gt;
&lt;h3&gt;Tip 1: Avoid Recompiling Patterns&lt;/h3&gt;
&lt;p&gt;If you use the same regex multiple times, compile it once with &lt;code&gt;re.compile()&lt;/code&gt;. Here&amp;#39;s a typical use case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re
import time

# Sample texts
texts = [&amp;quot;123&amp;quot;, &amp;quot;abc 456&amp;quot;, &amp;quot;def 789 ghi&amp;quot;] * 10000

# Pattern without compiling
pattern1 = r&amp;quot;\d+&amp;quot;

start_time = time.time()
for text in texts:
    match = re.search(pattern1, text)
end_time = time.time()
time_without_compile = end_time - start_time

# Compiling the pattern
pattern2 = re.compile(r&amp;quot;\d+&amp;quot;)

start_time = time.time()
for text in texts:
    match = pattern2.search(text)
end_time = time.time()
time_with_compile = end_time - start_time

print(f&amp;quot;without compile:{time_without_compile: .3}, with compile:{time_with_compile: .3}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;without compile: 0.0286, with compile: 0.0178
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, every time the code iterates through the list, the pattern is recompiled if you don&amp;#39;t use the &lt;code&gt;re.compile()&lt;/code&gt; function, lowering execution time.&lt;/p&gt;
&lt;h3&gt;Tip 2: Use Specific Patterns&lt;/h3&gt;
&lt;p&gt;Using specific regex patterns rather than broad ones can significantly improve performance. Specific patterns reduce the number of possible matches the regex engine has to consider, speeding up the matching process.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s consider the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re
import time

# Sample texts
texts = [&amp;quot;abc123xyz&amp;quot;, &amp;quot;123&amp;quot;, &amp;quot;a123b&amp;quot;, &amp;quot;x123y&amp;quot;, &amp;quot;nonumber&amp;quot;, &amp;quot;123&amp;quot;, &amp;quot;test&amp;quot;] * 50000

# Less efficient pattern
pattern1 = r&amp;quot;.*123.*&amp;quot;

start_time = time.time()
for text in texts:
    match = re.search(pattern1, text)
end_time = time.time()
time_with_less_efficient_pattern = end_time - start_time

# More efficient pattern
pattern2 = r&amp;quot;\b123\b&amp;quot;

start_time = time.time()
for text in texts:
    match = re.search(pattern2, text)
end_time = time.time()
time_with_more_efficient_pattern = end_time - start_time

print(f&amp;quot;time with not efficient pattern: {time_with_less_efficient_pattern: .3}, time with efficient pattern: {time_with_more_efficient_pattern: .3}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;time with not efficient pattern:  0.412, time with efficient pattern:  0.385
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, we have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.*123.*&lt;/code&gt;: A less efficient pattern because &lt;code&gt;.*&lt;/code&gt; matches any character (except a newline) 0 or more times, both before and after &amp;quot;123&amp;quot;. The regex engine has to consider a large number of potential matches.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\b123\b&lt;/code&gt; is more efficient because it is more specific. &lt;code&gt;\b&lt;/code&gt; asserts a word boundary, ensuring that &amp;quot;123&amp;quot; is matched only as a whole word. This reduces the number of possible matches the regex engine needs to evaluate.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tip 3: Always Use Raw Strings&lt;/h3&gt;
&lt;p&gt;Using raw strings (by prefixing the pattern with &lt;code&gt;r&lt;/code&gt;) is a good practice, especially when dealing with backslashes and escape sequences. When you use raw strings, Python treats backslashes as literal characters rather than escape characters. This can prevent unexpected behavior or errors in your regex patterns and also results in performance improvements.&lt;/p&gt;
&lt;p&gt;This example shows that performance is better with raw strings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re
import time

# Define a regex pattern without using a raw string
pattern_without_raw = &amp;quot;\d+&amp;quot;

# Define the same regex pattern using a raw string
pattern_with_raw = r&amp;quot;\d+&amp;quot;

# Create some sample text to search
text = &amp;quot;123 456 789 012&amp;quot; * 3000000

# Search using the pattern without raw string
start_time = time.time()
matches_without_raw = re.findall(pattern_without_raw, text)
end_time = time.time()
time_without_raw = end_time - start_time

# Search using the pattern with raw string
start_time = time.time()
matches_with_raw = re.findall(pattern_with_raw, text)
end_time = time.time()
time_with_raw = end_time - start_time

# Print the results and performance comparison
print(f&amp;quot;Time taken without raw string:{time_without_raw: .3}&amp;quot;)
print(f&amp;quot;Time taken with raw string: {time_with_raw: .3}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s the result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Time taken without raw string: 1.65
Time taken with raw string:  1.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the side of error management, here&amp;#39;s an example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

# Define a regex pattern to match email addresses
pattern_without_raw = &amp;quot;\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b&amp;quot;

# Sample text containing email addresses
text = &amp;quot;Contact us at email@example.com or visit our website www.example.com&amp;quot;

# Try to find email addresses using the pattern without a raw string
matches_without_raw = re.findall(pattern_without_raw, text)

# Print the matches
print(f&amp;quot;Matches without raw string: { matches_without_raw}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Matches without raw string: []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We haven&amp;#39;t used a raw string, so Python treats the backslashes in the pattern as escape characters. This leads to unintended behavior: specifically, &lt;code&gt;\b&lt;/code&gt; is interpreted as a backspace character instead of a word boundary, and the pattern fails to match email addresses correctly.&lt;/p&gt;
&lt;p&gt;The correct pattern to avoid this unintended behavior is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pattern = r&amp;quot;\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&amp;#39;s it for our whistle-stop tour of regex in Python!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we covered the basics, advanced techniques, real-world use cases, and performance optimization strategies of regular expressions in Python.&lt;/p&gt;
&lt;p&gt;By understanding and utilizing these concepts, you can handle complex text-processing tasks with ease and precision.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Find and Fix N+1 Queries in Django Using AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/12/04/find-and-fix-n-plus-one-queries-in-django-using-appsignal.html"/>
    <id>https://blog.appsignal.com/2024/12/04/find-and-fix-n-plus-one-queries-in-django-using-appsignal.html</id>
    <published>2024-12-04T00:00:00+00:00</published>
    <updated>2024-12-04T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll track the N+1 query problem in a Django app and fix it using AppSignal.</summary>
    <content type="html">&lt;p&gt;In this article, you&amp;#39;ll learn about N+1 queries, how to detect them with AppSignal, and how to fix them to speed up your Django apps significantly.&lt;/p&gt;
&lt;p&gt;We&amp;#39;ll start with the theoretical aspects and then move on to practical examples. The practical examples will mirror scenarios you might encounter in a production environment.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get started!&lt;/p&gt;
&lt;h2&gt;What Are N+1 Queries?&lt;/h2&gt;
&lt;p&gt;The N+1 query problem is a prevalent performance issue in web applications that interact with a database. These queries can cause significant bottlenecks, which intensify as your database grows.&lt;/p&gt;
&lt;p&gt;The problem occurs when you retrieve a collection of objects and then access the related objects for each item in the collection. For instance, fetching a list of books requires a single query (&lt;code&gt;1&lt;/code&gt; query), but accessing the author for each book triggers an additional query for every item (&lt;code&gt;N&lt;/code&gt; queries).&lt;/p&gt;
&lt;p&gt;N+1 problems can also occur when creating or updating data in a database. For example, iterating through a loop to create or update objects individually, rather than using methods like &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/ref/models/querysets/#bulk-create&quot;&gt;&lt;code&gt;bulk_create()&lt;/code&gt;&lt;/a&gt; or &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/ref/models/querysets/#bulk-update&quot;&gt;&lt;code&gt;bulk_update()&lt;/code&gt;&lt;/a&gt;, can result in excessive queries.&lt;/p&gt;
&lt;p&gt;N+1 queries are highly inefficient because executing numerous small queries is significantly slower and more resource-intensive than consolidating operations into fewer, larger queries.&lt;/p&gt;
&lt;p&gt;Django&amp;#39;s default &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/ref/models/querysets/&quot;&gt;&lt;code&gt;QuerySet&lt;/code&gt;&lt;/a&gt; behavior can inadvertently lead to N+1 issues, especially if you&amp;#39;re unaware of how QuerySets work. Querysets in Django are lazy, meaning no database queries are executed until the QuerySet is evaluated.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Ensure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.9+&lt;/a&gt; and &lt;a href=&quot;https://www.git-scm.com/downloads&quot;&gt;Git&lt;/a&gt; installed on your local machine&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/support/operating-systems.html&quot;&gt;An AppSignal-supported operating system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://appsignal.com/users/sign_in&quot;&gt;An AppSignal account&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; The source code for this project can be found in the &lt;a href=&quot;https://github.com/duplxey/appsignal-django-n-plus-one&quot;&gt;appsignal-django-n-plus-one GitHub repository&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll work with a book management web app. The web app is built to demonstrate the N+1 query problem and how to resolve it.&lt;/p&gt;
&lt;p&gt;Start by cloning the &lt;code&gt;base&lt;/code&gt; branch of the GitHub repo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ git clone git@github.com:duplxey/appsignal-django-n-plus-one.git \
    --single-branch --branch base &amp;amp;&amp;amp; cd appsignal-django-n-plus-one
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create and activate a virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ python3 -m venv venv &amp;amp;&amp;amp; source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the requirements:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Migrate and populate the database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python manage.py migrate
(venv)$ python manage.py populate_db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, start the development server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open your favorite web browser and navigate to &lt;a href=&quot;http://localhost:8000/books&quot;&gt;http://localhost:8000/books&lt;/a&gt;. The web app should return a JSON list of 500 books from the database.&lt;/p&gt;
&lt;p&gt;The Django admin site is accessible at &lt;a href=&quot;http://localhost:8000/admin&quot;&gt;http://localhost:8000/admin&lt;/a&gt;. The admin credentials are:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;user: username
pass: password
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Install AppSignal for Django&lt;/h2&gt;
&lt;p&gt;To install AppSignal on your Django project, follow the official docs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/installation&quot;&gt;AppSignal Python installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/django.html&quot;&gt;AppSignal Django instrumentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/sqlite.html&quot;&gt;AppSignal SQLite instrumentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ensure everything works by restarting the development server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your app should automatically send a demo error to AppSignal. From this point forward, all your errors will be sent to AppSignal. Additionally, AppSignal will monitor your app&amp;#39;s performance and detect any issues.&lt;/p&gt;
&lt;h2&gt;Web App Logic&lt;/h2&gt;
&lt;p&gt;The prerequisite to fixing N+1 queries is understanding your app&amp;#39;s database schema. Pay close attention to your models&amp;#39; relationships: they can help you pinpoint potential N+1 problems.&lt;/p&gt;
&lt;h3&gt;Models&lt;/h3&gt;
&lt;p&gt;The web app has two models — &lt;code&gt;Author&lt;/code&gt; and &lt;code&gt;Book&lt;/code&gt; — which share a one-to-many (&lt;code&gt;1:M&lt;/code&gt;) relationship. This means each book is associated with a single author, while an author can be linked to multiple books.&lt;/p&gt;
&lt;p&gt;Both models have a &lt;code&gt;to_dict()&lt;/code&gt; method for serializing model instances to JSON. On top of that, the &lt;code&gt;Book&lt;/code&gt; model uses deep serialization (serializing the book as well as the book&amp;#39;s author).&lt;/p&gt;
&lt;p&gt;The models are defined in &lt;code&gt;books/models.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# books/models.py
class Author(models.Model):
    first_name = models.CharField(max_length=64)
    last_name = models.CharField(max_length=64)
    birth_date = models.DateField()

    def full_name(self):
        return f&amp;quot;{self.first_name} {self.last_name}&amp;quot;

    def to_dict(self):
        return {
            &amp;quot;id&amp;quot;: self.id,
            &amp;quot;first_name&amp;quot;: self.first_name,
            &amp;quot;last_name&amp;quot;: self.last_name,
            &amp;quot;birth_date&amp;quot;: self.birth_date,
        }

    def __str__(self):
        return f&amp;quot;{self.first_name} {self.last_name}&amp;quot;


class Book(models.Model):
    title = models.CharField(max_length=128)
    author = models.ForeignKey(
        to=Author,
        related_name=&amp;quot;books&amp;quot;,
        on_delete=models.CASCADE,
    )
    summary = models.TextField(max_length=512, blank=True, null=True)
    isbn = models.CharField(max_length=13, unique=True, help_text=&amp;quot;ISBN-13&amp;quot;)
    published_at = models.DateField()

    def to_dict(self):
        return {
            &amp;quot;id&amp;quot;: self.id,
            &amp;quot;title&amp;quot;: self.title,
            &amp;quot;author&amp;quot;: self.author.to_dict(),
            &amp;quot;summary&amp;quot;: self.summary,
            &amp;quot;isbn&amp;quot;: self.isbn,
            &amp;quot;published_at&amp;quot;: self.published_at,
        }

    def __str__(self):
        return f&amp;quot;{self.author}: {self.title}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;They are then registered for the Django admin site in &lt;code&gt;books/admin.py&lt;/code&gt;, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# books/admin.py
class BookInline(admin.TabularInline):
    model = Book
    extra = 0


class AuthorAdmin(admin.ModelAdmin):
    list_display = [&amp;quot;full_name&amp;quot;, &amp;quot;birth_date&amp;quot;]
    inlines = [BookInline]


class BookAdmin(admin.ModelAdmin):
    list_display = [&amp;quot;title&amp;quot;, &amp;quot;author&amp;quot;, &amp;quot;published_at&amp;quot;]


admin.site.register(Author, AuthorAdmin)
admin.site.register(Book, BookAdmin)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that &lt;code&gt;AuthorAdmin&lt;/code&gt; uses &lt;code&gt;BookInline&lt;/code&gt; to display the author&amp;#39;s books within the author&amp;#39;s admin page.&lt;/p&gt;
&lt;h3&gt;Views&lt;/h3&gt;
&lt;p&gt;The web app provides the following endpoints:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/&quot;&gt;&lt;code&gt;/books/&lt;/code&gt;&lt;/a&gt; returns the list of books&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/1/&quot;&gt;&lt;code&gt;/books/&amp;lt;book_id&amp;gt;/&lt;/code&gt;&lt;/a&gt; returns a specific book&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/by-authors/&quot;&gt;&lt;code&gt;/books/by-authors/&lt;/code&gt;&lt;/a&gt; returns a list of books grouped by authors&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/authors/&quot;&gt;&lt;code&gt;/books/authors/&lt;/code&gt;&lt;/a&gt; returns the list of authors&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/authors/1/&quot;&gt;&lt;code&gt;/books/authors/&amp;lt;author_id&amp;gt;/&lt;/code&gt;&lt;/a&gt; returns a specific author&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;The links above are clickable if you have the development web server running.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And they&amp;#39;re defined in &lt;code&gt;books/views.py&lt;/code&gt; like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# books/views.py
def book_list_view(request):
    books = Book.objects.all()
    return JsonResponse(
        {
            &amp;quot;count&amp;quot;: books.count(),
            &amp;quot;results&amp;quot;: [book.to_dict() for book in books],
        }
    )


def book_details_view(request, book_id):
    try:
        book = Book.objects.get(id=book_id)
        return JsonResponse(book.to_dict())
    except Book.DoesNotExist:
        return JsonResponse({&amp;quot;error&amp;quot;: &amp;quot;Book not found&amp;quot;}, status=404)


def book_by_author_list_view(request):
    try:
        authors = Author.objects.all()
        return JsonResponse(
            {
                &amp;quot;count&amp;quot;: authors.count(),
                &amp;quot;results&amp;quot;: [
                    {
                        &amp;quot;author&amp;quot;: author.to_dict(),
                        &amp;quot;books&amp;quot;: [book.to_dict() for book in author.books.all()],
                    }
                    for author in authors
                ],
            }
        )
    except Author.DoesNotExist:
        return JsonResponse({&amp;quot;error&amp;quot;: &amp;quot;Author not found&amp;quot;}, status=404)


def author_list_view(request):
    authors = Author.objects.all()
    return JsonResponse(
        {
            &amp;quot;count&amp;quot;: authors.count(),
            &amp;quot;results&amp;quot;: [author.to_dict() for author in authors],
        }
    )


def author_details_view(request, author_id):
    try:
        author = Author.objects.get(id=author_id)
        return JsonResponse(author.to_dict())
    except Author.DoesNotExist:
        return JsonResponse({&amp;quot;error&amp;quot;: &amp;quot;Author not found&amp;quot;}, status=404)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Great, you now know how the web app works!&lt;/p&gt;
&lt;p&gt;In the next section, we&amp;#39;ll benchmark our app to detect N+1 queries with AppSignal and then modify the code to eliminate them.&lt;/p&gt;
&lt;h2&gt;Detect N+1 Queries in Your Django App with AppSignal&lt;/h2&gt;
&lt;p&gt;Detecting performance issues with AppSignal is easy. All you have to do is use/test the app as you normally would (for example, perform end-user testing by visiting all the endpoints and validating the responses).&lt;/p&gt;
&lt;p&gt;When an endpoint is hit, AppSignal will create a performance report for it and group all related visits together. Each visit will be recorded as a sample in the endpoint&amp;#39;s report.&lt;/p&gt;
&lt;h3&gt;Detect N+1 Queries in Views&lt;/h3&gt;
&lt;p&gt;Firstly, visit all your app&amp;#39;s endpoints to generate the performance reports:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/&quot;&gt;&lt;code&gt;/books/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/1/&quot;&gt;&lt;code&gt;/books/&amp;lt;book_id&amp;gt;/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/by-authors/&quot;&gt;&lt;code&gt;/books/by-authors/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/authors/&quot;&gt;&lt;code&gt;/books/authors/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://localhost:8000/books/authors/1/&quot;&gt;&lt;code&gt;/books/authors/&amp;lt;author_id&amp;gt;/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Next, let&amp;#39;s use the AppSignal dashboard to analyze slow endpoints.&lt;/p&gt;
&lt;h4&gt;Example 1: One-To-One Relationship (&lt;code&gt;select_related()&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;Navigate to your AppSignal app and select &lt;strong&gt;Performance &amp;gt; Issue list&lt;/strong&gt; on the sidebar. Then click &lt;strong&gt;Mean&lt;/strong&gt; to sort the issues by descending mean response time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-11/django-issues.png&quot; alt=&quot;AppSignal Performance Issue List&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Click on the slowest endpoint (&lt;code&gt;books/&lt;/code&gt;) to view its details.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-11/django-perf-issues.png&quot; alt=&quot;AppSignal Performance Issue List Details&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Looking at the latest sample, we can see that this endpoint returns a response in &lt;code&gt;1090&lt;/code&gt; milliseconds. The group breakdown shows that SQLite takes &lt;code&gt;651&lt;/code&gt; milliseconds while Django takes &lt;code&gt;439&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This indicates a problem because an endpoint as simple as this shouldn&amp;#39;t take as long.&lt;/p&gt;
&lt;p&gt;To get more details on what happened, select &lt;strong&gt;Samples&lt;/strong&gt; in the sidebar and then the latest sample.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-11/perf-get-books.png&quot; alt=&quot;AppSignal Performance Issue List Details Samples&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Scroll down to the &lt;strong&gt;Event Timeline&lt;/strong&gt; to see what SQL queries got executed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-11/event-timeline.png&quot; alt=&quot;AppSignal Performance Sample Events Timeline&quot;/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hovering over the &lt;code&gt;query.sql&lt;/code&gt; text displays the actual SQL query.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;More than 1000 queries were executed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM &amp;quot;books_book&amp;quot;;
SELECT * FROM &amp;quot;books_author&amp;quot; WHERE &amp;quot;books_author&amp;quot;.&amp;quot;id&amp;quot; = 3; -- 3= 1st book&amp;#39;s author id
SELECT * FROM &amp;quot;books_author&amp;quot; WHERE &amp;quot;books_author&amp;quot;.&amp;quot;id&amp;quot; = 6; -- 6= 2nd book&amp;#39;s author id
...
SELECT * FROM &amp;quot;books_author&amp;quot; WHERE &amp;quot;books_author&amp;quot;.&amp;quot;id&amp;quot; = n; -- n= n-th book&amp;#39;s author id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are a clear sign of N+1 queries. The first query fetched a book (&lt;code&gt;1&lt;/code&gt;), and each subsequent query fetched the book&amp;#39;s author&amp;#39;s details (&lt;code&gt;N&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;To fix it, navigate to &lt;code&gt;books/views.py&lt;/code&gt; and modify &lt;code&gt;book_list_view()&lt;/code&gt; like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# books/views.py
def book_list_view(request):
    books = Book.objects.all().select_related(&amp;quot;author&amp;quot;)  # modified
    return JsonResponse(
        {
            &amp;quot;count&amp;quot;: books.count(),
            &amp;quot;results&amp;quot;: [book.to_dict() for book in books],
        }
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By utilizing Django&amp;#39;s &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/ref/models/querysets/#select-related&quot;&gt;&lt;code&gt;select_related()&lt;/code&gt;&lt;/a&gt; method, we select the additional related object data (i.e., &lt;code&gt;author&lt;/code&gt;) in the initial query. The ORM will now leverage a SQL join, and the final query will look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM &amp;quot;books_book&amp;quot;
    INNER JOIN &amp;quot;books_author&amp;quot; ON (&amp;quot;books_book&amp;quot;.&amp;quot;author_id&amp;quot; = &amp;quot;books_author&amp;quot;.&amp;quot;id&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait for the development server to restart and retest the affected endpoint.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-11/event-request-django.png&quot; alt=&quot;AppSignal After Fix Benchmark&quot;/&gt;&lt;/p&gt;
&lt;p&gt;After benchmarking again, the response time goes from &lt;code&gt;1090&lt;/code&gt; to &lt;code&gt;45&lt;/code&gt;, and the number of queries lowers from &lt;code&gt;1024&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;. This is a 24x and 512x improvement, respectively.&lt;/p&gt;
&lt;h4&gt;Example 2: Many-To-One Relationship (&lt;code&gt;prefetch_related()&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;Next, let&amp;#39;s look at the second slowest endpoint (&lt;code&gt;books/by-authors/&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Use the dashboard as we did in the previous step to inspect the endpoint&amp;#39;s SQL queries. You&amp;#39;ll notice a similar but less severe N+1 pattern with this endpoint.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This endpoint&amp;#39;s performance is less severe because Django is smart enough to cache the frequently executed SQL queries, i.e., repeatedly fetching the author of a book. &lt;a href=&quot;https://docs.djangoproject.com/en/4.1/topics/db/optimization/&quot;&gt;Check out the official docs to learn more about Django caching&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let&amp;#39;s utilize &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/ref/models/querysets/#prefetch-related&quot;&gt;&lt;code&gt;prefetch_related()&lt;/code&gt;&lt;/a&gt; in &lt;code&gt;books/views.py&lt;/code&gt; to speed up the endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# books/views.py
def book_by_author_list_view(request):
    try:
        authors = Author.objects.all().prefetch_related(&amp;quot;books&amp;quot;)  # modified
        return JsonResponse(
            {
                &amp;quot;count&amp;quot;: authors.count(),
                &amp;quot;results&amp;quot;: [
                    {
                        &amp;quot;author&amp;quot;: author.to_dict(),
                        &amp;quot;books&amp;quot;: [book.to_dict() for book in author.books.all()],
                    }
                    for author in authors
                ],
            }
        )
    except Author.DoesNotExist:
        return JsonResponse({&amp;quot;error&amp;quot;: &amp;quot;Author not found&amp;quot;}, status=404)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the previous section, we used the &lt;code&gt;select_related()&lt;/code&gt; method to handle a one-to-one relationship (each book has a single author). However, in this case, we&amp;#39;re handling a one-to-many relationship (an author can have multiple books), so we must use &lt;code&gt;prefetch_related()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The difference between these two methods is that &lt;code&gt;select_related()&lt;/code&gt; works on the SQL level, while &lt;code&gt;prefetch_related()&lt;/code&gt; optimizes on the Python level. The latter method can also be used for many-to-many relationships.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For more information, &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/ref/models/querysets/#prefetch-related&quot;&gt;check out Django&amp;#39;s official docs on &lt;code&gt;prefetch_related()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After benchmarking, the response time goes from &lt;code&gt;90&lt;/code&gt; to &lt;code&gt;44&lt;/code&gt; milliseconds, and the number of queries lowers from &lt;code&gt;32&lt;/code&gt; to &lt;code&gt;4&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Detect N+1 Queries in Django Admin&lt;/h3&gt;
&lt;p&gt;Discovering N+1 queries in the Django admin site works similarly.&lt;/p&gt;
&lt;p&gt;First, &lt;a href=&quot;http://localhost:8000/admin/&quot;&gt;log in&lt;/a&gt; to your admin site and generate performance reports (for example, create a few authors or books, update, and delete them).&lt;/p&gt;
&lt;p&gt;Next, navigate to your AppSignal app dashboard, this time filtering the issues by &lt;code&gt;admin&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-11/perf-admin.png&quot; alt=&quot;AppSignal Performance Issue List Admin&quot;/&gt;&lt;/p&gt;
&lt;p&gt;In my case, the two slowest endpoints are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/admin/login&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/admin/books/author/&amp;lt;object_id&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We can&amp;#39;t do much about &lt;code&gt;/admin/login&lt;/code&gt;, since it&amp;#39;s entirely handled by Django, so let&amp;#39;s focus on the second slowest endpoint. Inspecting it will reveal an N+1 query problem. The author is fetched separately for each book.&lt;/p&gt;
&lt;p&gt;To fix this, override &lt;code&gt;get_queryset()&lt;/code&gt; in &lt;code&gt;BookInline&lt;/code&gt; to fetch author details in the initial query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# books/admin.py
class BookInline(admin.TabularInline):
    model = Book
    extra = 0

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        return queryset.select_related(&amp;quot;author&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Benchmark once again and verify that the number of queries has decreased.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this post, we&amp;#39;ve discussed detecting and fixing N+1 queries in Django using AppSignal.&lt;/p&gt;
&lt;p&gt;Leveraging what you&amp;#39;ve learned here can help you significantly speed up your Django web apps.&lt;/p&gt;
&lt;p&gt;The two most essential methods to keep in mind are &lt;code&gt;select_related()&lt;/code&gt; and &lt;code&gt;prefetch_related()&lt;/code&gt;. The first is used for one-to-one relationships, and the second is for one-to-many and many-to-many relationships.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Advanced Open edX Monitoring with AppSignal for Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/10/30/advanced-open-edx-monitoring-with-appsignal-for-python.html"/>
    <id>https://blog.appsignal.com/2024/10/30/advanced-open-edx-monitoring-with-appsignal-for-python.html</id>
    <published>2024-10-30T00:00:00+00:00</published>
    <updated>2024-10-30T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">In this second part of a two-part series, we&#039;ll dive into the more advanced monitoring capabilities that AppSignal can offer our Open edX application.</summary>
    <content type="html">&lt;p&gt;In the first part of this series, we explored how AppSignal can significantly enhance the robustness of Open edX platforms. We saw the challenges that Open edX faces as it scales and how AppSignal&amp;#39;s features — including real-time performance monitoring and automated error tracking — provide essential tools for DevOps teams. Our walkthrough covered the initial setup and integration of AppSignal with Open edX, highlighting the immediate benefits of this powerful observability framework.&lt;/p&gt;
&lt;p&gt;In this second post, we&amp;#39;ll dive deeper into the advanced monitoring capabilities that AppSignal offers. This includes streaming logs from Open edX to AppSignal, monitoring background workers with Celery, and tracking Redis queries. We will demonstrate how these features can be leveraged to address specific operational challenges, ensuring that our learning platform remains fail-safe under varying circumstances.&lt;/p&gt;
&lt;p&gt;By the end of this article, you will know how to utilize AppSignal to its full potential in maintaining and improving the performance and reliability of your Open edX platform.&lt;/p&gt;
&lt;h2&gt;Streaming Logs to AppSignal&lt;/h2&gt;
&lt;p&gt;One of AppSignal&amp;#39;s strongest features is centralized log management.&lt;/p&gt;
&lt;p&gt;Commonly at Open edX, the support team reports an issue with the site, and an engineer can SSH into the server right away to check for Nginx, Mongo, MySQL, and Open edX Application logs.&lt;/p&gt;
&lt;p&gt;A centralized storage place that houses logs without the need for you to SSH into the server is a really powerful feature. We can also set up notifications based on an issue&amp;#39;s severity.&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s see how we can stream our logs from Open edX to AppSignal.&lt;/p&gt;
&lt;h3&gt;Create a Source&lt;/h3&gt;
&lt;p&gt;Under the &lt;em&gt;Logging&lt;/em&gt; section, click on &lt;strong&gt;Manage sources&lt;/strong&gt; and create a new source, with &lt;strong&gt;HTTP&lt;/strong&gt; as the platform and &lt;strong&gt;JSON&lt;/strong&gt; as the format. After creating the source, AppSignal provides an endpoint and API KEY that we can &lt;strong&gt;POST&lt;/strong&gt; our logs to.&lt;/p&gt;
&lt;p&gt;To have more control over log transmission, we can write a simple Python script that reads logs from our local Open edX, pre-processes them, and moves the important ones to AppSignal. For example, I wrote the following script to move only &lt;code&gt;ERROR&lt;/code&gt; logs to AppSignal (skipping &lt;code&gt;INFO&lt;/code&gt; and &lt;code&gt;WARNING&lt;/code&gt; logs):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import requests
import json
from datetime import datetime
import logging

# Setup logging configuration
logging.basicConfig(level=logging.INFO, format=&amp;#39;%(asctime)s - %(levelname)s - %(message)s&amp;#39;)

# File to keep track of the last processed line
log_pointer_file = &amp;#39;/root/.local/share/tutor/data/lms/logs/processed.log&amp;#39;
log_file = &amp;#39;/root/.local/share/tutor/data/lms/logs/all.log&amp;#39;

# APpSignal API KEY
api_key = &amp;quot;MY-API-KEY&amp;quot;  # Replace with your actual API key
# URL to post the logs
url = f&amp;#39;https://appsignal-endpoint.net/logs?api_key={api_key}&amp;#39;

def read_last_processed():
    try:
        with open(log_pointer_file, &amp;#39;r&amp;#39;) as file:
            content = file.read().strip()
            last_processed = int(content) if content else 0
            logging.info(f&amp;quot;Last processed line number read: {last_processed}&amp;quot;)
            return last_processed
    except (FileNotFoundError, ValueError) as e:
        logging.error(f&amp;quot;Could not read from log pointer file: {e}&amp;quot;)
        return 0

def update_last_processed(line_number):
    try:
        with open(log_pointer_file, &amp;#39;w&amp;#39;) as file:
            file.write(str(line_number))
            logging.info(f&amp;quot;Updated last processed to line number: {line_number}&amp;quot;)
    except Exception as e:
        logging.error(f&amp;quot;Could not update log pointer file: {e}&amp;quot;)

def parse_log_line(line):
    if &amp;#39;ERROR&amp;#39; in line:
        parts = line.split(&amp;#39;ERROR&amp;#39;, 1)
        timestamp = parts[0].strip()
        message_parts = parts[1].strip().split(&amp;#39; - &amp;#39;, 1)
        message = message_parts[1] if len(message_parts) &amp;gt; 1 else &amp;#39;&amp;#39;
        attributes_part = message_parts[0].strip(&amp;#39;[]&amp;#39;).split(&amp;#39;] [&amp;#39;)
        # Flatten attributes into a dictionary with string keys and values
        attributes = {}
        for attr in attributes_part:
            key_value = attr.split(None, 1)
            if len(key_value) == 2:
                key, value = key_value
                key = key.rstrip(&amp;#39;]:&amp;#39;).replace(&amp;#39; &amp;#39;, &amp;#39;_&amp;#39;).replace(&amp;#39;.&amp;#39;, &amp;#39;_&amp;#39;)  # Replace spaces and dots in keys
                if len(key) &amp;lt;= 50:
                    attributes[key] = value
        # Format the timestamp
        formatted_timestamp = datetime.strptime(timestamp, &amp;#39;%Y-%m-%d %H:%M:%S,%f&amp;#39;).isoformat()[:-3] + &amp;#39;Z&amp;#39;
        # Add the message and attributes to the log structure
        json_data = {
            &amp;quot;timestamp&amp;quot;: formatted_timestamp,
            &amp;quot;group&amp;quot;: &amp;quot;openedx&amp;quot;,
            &amp;quot;severity&amp;quot;: &amp;quot;error&amp;quot;,
            &amp;quot;hostname&amp;quot;: &amp;quot;tutor&amp;quot;,
            &amp;quot;message&amp;quot;: message,
        }
        json_data.update(attributes)  # Add the attributes directly to the json_data dictionary
        return json_data

def post_logs(json_data):
    headers = {&amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39;}
    response = requests.post(url, json=json_data, headers=headers)
    logging.info(f&amp;quot;Posted log to server; HTTP status code: {response.status_code}, Response: {response.content}&amp;quot;)
    return response.status_code

def process_logs():
    last_processed = read_last_processed()
    with open(log_file, &amp;#39;r&amp;#39;) as file:
        for i, line in enumerate(file, 1):
            if i &amp;gt; last_processed:
                json_data = parse_log_line(line)
                if json_data:
                    response_code = post_logs(json_data)
                    if response_code == 200:
                        update_last_processed(i)
                    else:
                        logging.warning(f&amp;quot;Failed to post log, HTTP status code: {response_code}&amp;quot;)

if __name__ == &amp;#39;__main__&amp;#39;:
    logging.info(&amp;quot;Starting log processing script.&amp;quot;)
    process_logs()
    logging.info(&amp;quot;Finished log processing.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s how the script works:&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Log File Management&lt;/strong&gt;: Tutor saves all of the logs in the &lt;code&gt;/root/.local/share/tutor/data/lms/logs/all.log&lt;/code&gt; file. This file contains MySQL, LMS, CMS, Caddy, Celery, and other services. The script uses a pointer &lt;code&gt;/root/.local/share/tutor/data/lms/logs/processed.log&lt;/code&gt; file that tracks the last processed line. This ensures that each log is processed only once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Filtering&lt;/strong&gt;: As mentioned, we only send &lt;code&gt;ERROR&lt;/code&gt; logs to AppSignal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Parsing and Formatting&lt;/strong&gt;: Each error log is parsed to extract key pieces of information, such as the timestamp and error message. The script formats this data into a JSON structure suitable for transmission.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log Transmission&lt;/strong&gt;: The formatted log data is sent to AppSignal using an HTTP POST request.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Important: Please make sure you don&amp;#39;t send any personally identifiable information to the endpoint.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now run this script and it should move &lt;code&gt;ERROR&lt;/code&gt; logs to AppSignal:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/error_logs.png&quot; alt=&quot;error logs&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You can also create a new trigger to notify you as soon as a specific event like &lt;code&gt;ERROR&lt;/code&gt; happens:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/error_trigger.png&quot; alt=&quot;error trigger&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Monitor Celery and Redis using AppSignal&lt;/h2&gt;
&lt;p&gt;Celery (a distributed task queue) is a vital component of Open edX, responsible for managing background tasks such as grading, certificate generation, and bulk email dispatch. Redis often acts as the broker for Celery, managing task queues. Both systems are essential for asynchronous processing and can become bottlenecks during periods of high usage. Monitoring these services with AppSignal provides valuable insights into task execution and queue health, helping you preemptively address potential issues. Let&amp;#39;s see how we can monitor Celery and Redis.&lt;/p&gt;
&lt;p&gt;First, install the necessary packages. Add the following to the &lt;code&gt;OPENEDX_EXTRA_PIP_REQUIREMENTS&lt;/code&gt; variable in the &lt;code&gt;.local/share/tutor/config.yml&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- opentelemetry-instrumentation-celery==0.45b0
- opentelemetry-instrumentation-redis==0.45b0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It should look like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;OPENEDX_EXTRA_PIP_REQUIREMENTS:
  - appsignal==1.3.0
  - opentelemetry-instrumentation-django==0.45b0
  - opentelemetry-instrumentation-celery==0.45b0
  - opentelemetry-instrumentation-redis==0.45b0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, we are installing &lt;code&gt;opentelemetry&lt;/code&gt; packages for Celery and Redis.&lt;/p&gt;
&lt;p&gt;Now, we can instrument Celery with &lt;code&gt;worker_process_init&lt;/code&gt; to report its metrics to AppSignal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/celery_instrumentation.png&quot; alt=&quot;celery instrumentation&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Heading back to our dashboard in AppSignal, we should see Celery and Redis reports in the &lt;em&gt;Performance&lt;/em&gt; section, with &lt;strong&gt;background&lt;/strong&gt; as the namespace.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/celery_report.png&quot; alt=&quot;celery report&quot;/&gt;&lt;/p&gt;
&lt;p&gt;For Redis queries, you can click on &lt;strong&gt;Slow queries&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/redis_report.png&quot; alt=&quot;redis report&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Practical Monitoring: Enhancing Open edX with AppSignal&lt;/h2&gt;
&lt;p&gt;In this section, we&amp;#39;ll revisit the initial issues outlined in &lt;a href=&quot;https://blog.appsignal.com/2024/10/02/integrating-open-edx-with-appsignal.html&quot;&gt;part one of this series&lt;/a&gt; and apply practical AppSignal monitoring solutions to ensure our Open edX platform stays robust and reliable. Here’s a breakdown.&lt;/p&gt;
&lt;h3&gt;Site Performance Improvement&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s begin by assessing overall site performance. In the &lt;em&gt;Performance&lt;/em&gt; section, under the &lt;strong&gt;Issue list&lt;/strong&gt;, we can see key metrics for all visited URLs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Response Time&lt;/strong&gt;: Directly reflects user experience by measuring the time taken to process and respond to requests. Factors influencing this include database queries and middleware operations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput&lt;/strong&gt;: Indicates the number of requests handled within a given timeframe.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mean Response Time&lt;/strong&gt;: Provides an average response time across all requests to a specific endpoint. Any mean response time over 1 second is a potential concern and highlights areas that need optimization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90th Percentile Response Time&lt;/strong&gt;: For example, a 90th percentile response time of 7 ms for &lt;code&gt;GET store/&lt;/code&gt; suggests that 90% of requests complete in 7 ms or less.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now let&amp;#39;s order all the actions based on the mean. Any item higher than 1 second should be considered a red flag:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/performance_higher_1_sec_1.png&quot; alt=&quot;performance_higher_1_sec_1&quot;/&gt;
&lt;img src=&quot;/images/blog/2024-10/performance_higher_1_sec_2.png&quot; alt=&quot;performance_higher_1_sec_2&quot;/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As we see, Celery tasks to rescore and reset student attempts, LMS requests to show course content, and some APIs are taking more than 1 second. Also, we should note that this is only for one active user. If we have more concurrent users, this response time will go up. Our first solution is to add more resources to the server (CPU and memory) and do another performance test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After identifying actions with mean response times exceeding 1 second, consider performance optimization strategies such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Minimizing JavaScript execution&lt;/li&gt;
&lt;li&gt;Using CDNs for static content&lt;/li&gt;
&lt;li&gt;Implementing caching techniques.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Server Resource Monitoring&lt;/h3&gt;
&lt;p&gt;We talked about anomaly detection and host monitoring in the previous article. Let&amp;#39;s add triggers for the following items:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU usage&lt;/li&gt;
&lt;li&gt;Disk usage&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;li&gt;Network traffic&lt;/li&gt;
&lt;li&gt;Error rate&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Custom Metrics&lt;/h3&gt;
&lt;p&gt;Two really important metrics for our platform are our number of active users and enrollments. Let&amp;#39;s see how we can measure these metrics using AppSignal.&lt;/p&gt;
&lt;p&gt;First, add &lt;code&gt;increment_counter&lt;/code&gt; to &lt;code&gt;common/djangoapps/student/views/management.py&lt;/code&gt; and &lt;code&gt;openedx/core/djangoapps/user_authn/views/login.py&lt;/code&gt; to track and increment the number of logins and enrollments when there is a new event.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/enrollment_count.png&quot; alt=&quot;enrollment_count&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/login_count.png&quot; alt=&quot;login_count&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s log in to Open edX and enroll in a course. Next, let&amp;#39;s head to our dashboard in AppSignal. Click on &lt;strong&gt;Add dashboard&lt;/strong&gt;, then &lt;strong&gt;Create dashboard&lt;/strong&gt;, and give it a name and description.&lt;/p&gt;
&lt;p&gt;Click on &lt;strong&gt;Add graph&lt;/strong&gt;, enter &lt;strong&gt;Active Users&lt;/strong&gt; as the title, select &lt;strong&gt;Add Metric&lt;/strong&gt; and use &lt;code&gt;login_count&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/login_count_metric.png&quot; alt=&quot;login_count_metric&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Your dashboard should look like the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/login_enrollment_dashboard.png&quot; alt=&quot;login dashboard&quot;/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can follow the same steps to add a graph for enrollments using an &lt;code&gt;enrollment_count&lt;/code&gt; metric.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Ensuring Consistent Styling&lt;/h3&gt;
&lt;p&gt;To make sure our site&amp;#39;s styling stays consistent, let&amp;#39;s add a new uptime check for &lt;code&gt;static/tailwind/css/lms-main-v1.css&lt;/code&gt; and get notified when a URL is broken:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/styling_uptime_check_1.png&quot; alt=&quot;styling_uptime_check_1&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/styling_uptime_check_2.png&quot; alt=&quot;styling_uptime_check_2&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Email Delivery and Error Handling&lt;/h3&gt;
&lt;p&gt;In the &lt;em&gt;Error&lt;/em&gt; section of the dashboard, we can view all errors, set up notifications for them, and work on fixes as soon as possible to prevent users from being negatively impacted.&lt;/p&gt;
&lt;h3&gt;Background Job Efficiency for Grading&lt;/h3&gt;
&lt;p&gt;In the &lt;strong&gt;Monitor Celery and Redis&lt;/strong&gt; section of this article, we saw how to instrument Celery and Redis using AppSignal. Let&amp;#39;s follow the same steps to enable AppSignal so we can see graded tasks. In the &lt;code&gt;lms/djangoapps/grades/tasks.py&lt;/code&gt; file, add the following lines:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/grading_monitoring.png&quot; alt=&quot;grading_monitoring&quot;/&gt;&lt;/p&gt;
&lt;p&gt;We should now see a couple of items to grade under &lt;strong&gt;Performance&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Issue list&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/celery_report.png&quot; alt=&quot;celery_report&quot;/&gt;&lt;/p&gt;
&lt;p&gt;As you can see, &lt;code&gt;recalculate_subsection_grade_v3&lt;/code&gt; (our main grading Celery task) takes 212 milliseconds. For regrading, &lt;code&gt;lms.djangoapps.instructor_task.tasks.reset_problem_attempts&lt;/code&gt; and &lt;code&gt;lms.djangoapps.instructor_task.tasks.rescore_problem&lt;/code&gt; take 1.77 seconds.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this two-part series, we integrated AppSignal with Open edX to fortify its monitoring capabilities. We started with the basics — setting up and understanding the fundamental offerings of AppSignal, including error tracking and performance monitoring.&lt;/p&gt;
&lt;p&gt;In this article, we tackled how to efficiently stream logs from various Open edX services to AppSignal, ensuring all relevant information was centralized and readily accessible. We also monitored crucial asynchronous tasks handled by Celery and Redis.&lt;/p&gt;
&lt;p&gt;Finally, we addressed some real-world challenges, such as slow site responses, resource bottlenecks during high enrollment periods, and unexpected issues like broken styling.&lt;/p&gt;
&lt;p&gt;By now, you should have a comprehensive understanding of how to leverage AppSignal to not just monitor, but also significantly improve, the performance and reliability of your Open edX platform.&lt;/p&gt;
&lt;p&gt;If you have any questions about Open edX or need further assistance, feel free to visit &lt;a href=&quot;https://cubite.io&quot;&gt;cubite.io&lt;/a&gt; or reach out to me directly at &lt;a href=&quot;mailto:amir@cubite.io&quot;&gt;amir@cubite.io&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to Use Lambda Functions in Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/10/16/how-to-use-lambda-functions-in-python.html"/>
    <id>https://blog.appsignal.com/2024/10/16/how-to-use-lambda-functions-in-python.html</id>
    <published>2024-10-16T00:00:00+00:00</published>
    <updated>2024-10-16T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s look at some examples and best practices when using Lambda functions for Python.</summary>
    <content type="html">&lt;p&gt;Lambda functions in Python are a powerful way to create small, anonymous functions on the fly. These functions are typically used for short, simple operations where the overhead of a full function definition would be unnecessary.&lt;/p&gt;
&lt;p&gt;While traditional functions are defined using the &lt;code&gt;def&lt;/code&gt; keyword, Lambda functions are defined using the &lt;code&gt;lambda&lt;/code&gt; keyword and are directly integrated into lines of code. In particular, they are often used as arguments for built-in functions. They enable developers to write clean and readable code by eliminating the need for temporary function definitions.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll cover what Lambda functions do and their syntax. We&amp;#39;ll also provide some examples and best practices for using them, and discuss their pros and cons.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Lambda functions have been a part of Python since version 2.0, so you&amp;#39;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Minimum Python version&lt;/strong&gt;: 2.0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recommended Python version&lt;/strong&gt;: 3.10 or later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this tutorial, we&amp;#39;ll see how to use Lambda functions with the library &lt;a href=&quot;https://pandas.pydata.org/&quot;&gt;Pandas&lt;/a&gt;: a fast, powerful, flexible, and easy-to-use open-source data analysis and manipulation library. If you don&amp;#39;t have it installed, run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pip install pandas
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Syntax and Basics of Lambda Functions for Python&lt;/h2&gt;
&lt;p&gt;First, let&amp;#39;s define the syntax developers must use to create Lambda functions.&lt;/p&gt;
&lt;p&gt;A Lambda function is defined using the &lt;code&gt;lambda&lt;/code&gt; keyword, followed by one or more arguments and an expression:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;lambda arguments: expression
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#39;s imagine we want to create a Lambda function that adds up two numbers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;add = lambda x, y: x + y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;result = add(3, 5)
print(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&amp;#39;ve created an anonymous function that takes two arguments, &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt;. Unlike traditional functions, Lambda functions don&amp;#39;t have a name: that&amp;#39;s why we say they are &amp;quot;anonymous.&amp;quot;&lt;/p&gt;
&lt;p&gt;Also, we don&amp;#39;t use the &lt;code&gt;return&lt;/code&gt; statement, as we do in regular Python functions. So we can use the Lambda function at will: it can be printed (as we did in this case), stored in a variable, etc.&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s see some common use cases for Lambda functions.&lt;/p&gt;
&lt;h2&gt;Common Use Cases for Lambda Functions&lt;/h2&gt;
&lt;p&gt;Lambda functions are particularly used in situations where we need a temporarily simple function. In particular, they are commonly used as arguments for higher-order functions.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s see some practical examples.&lt;/p&gt;
&lt;h3&gt;Using Lambda Functions with the &lt;code&gt;map()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;map()&lt;/code&gt; is a built-in function that applies a given function to each item of an iterable and returns a map object with the results.&lt;/p&gt;
&lt;p&gt;For example, let&amp;#39;s say we want to calculate the square roots of each number in a list. We could use a Lambda function like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Define the list of numbers
numbers = [1, 2, 3, 4]

# Calculate square values and print results
squared = list(map(lambda x: x ** 2, numbers))
print(squared)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[1, 4, 9, 16]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now have a list containing the square roots of the initial numbers.&lt;/p&gt;
&lt;p&gt;As we can see, this greatly simplifies processes to use functions on the fly that don&amp;#39;t need to be reused later.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h3&gt;Using Lambda Functions with the &lt;code&gt;filter()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;Now, suppose we have a list of numbers and want to filter even numbers.&lt;/p&gt;
&lt;p&gt;We can use a Lambda function as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Create a list of numbers
numbers = [1, 2, 3, 4]

# Filter for even numbers and print results
even = list(filter(lambda x: x % 2 == 0, numbers))
print(even)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[2,4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using Lambda Functions with the &lt;code&gt;sorted()&lt;/code&gt; Function&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;sorted()&lt;/code&gt; function in Python returns a new sorted list from the elements of any iterable. Using Lambda functions, we can apply specific filtering criteria to these lists.&lt;/p&gt;
&lt;p&gt;For example, suppose we have a list of points in two dimensions: &lt;code&gt;(x,y)&lt;/code&gt;. We want to create a list that orders the &lt;code&gt;y&lt;/code&gt; values incrementally.&lt;/p&gt;
&lt;p&gt;We can do it like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Creates a list of points
points = [(1, 2), (3, 1), (5, -1)]

# Sort the points and print
points_sorted = sorted(points, key=lambda point: point[1])
print(points_sorted)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[(5, -1), (3, 1), (1, 2)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using Lambda Functions in List Comprehensions&lt;/h3&gt;
&lt;p&gt;Given their conciseness, Lambda functions can be embedded in list comprehensions for on-the-fly computations.&lt;/p&gt;
&lt;p&gt;Suppose we have a list of numbers. We want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Iterate over the whole list&lt;/li&gt;
&lt;li&gt;Calculate and print double the initial values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;#39;s how we can do that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Create a list of numbers
numbers = [1, 2, 3, 4]

# Calculate and print the double of each one
squared = [(lambda x: x ** 2)(x) for x in numbers]
print(squared)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we obtain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[1, 4, 9, 16]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Advantages of Using Lambda Functions&lt;/h2&gt;
&lt;p&gt;Given the examples we&amp;#39;ve explored, let&amp;#39;s run through some advantages of using Lambda functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Conciseness and readability where the logic is simple&lt;/strong&gt;: Lambda functions allow for concise code, reducing the need for standard function definitions. This improves readability in cases where function logic is simple.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enhanced functional programming capabilities&lt;/strong&gt;: Lambda functions align well with functional programming principles, enabling functional constructs in Python code. In particular, they facilitate the use of higher-order functions and the application of functions as first-class objects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When and why to prefer Lambda functions&lt;/strong&gt;: Lambda functions are particularly advantageous when defining short, &amp;quot;throwaway&amp;quot; functions that don&amp;#39;t need to be reused elsewhere in code. So they are ideal for inline use, such as arguments to higher-order functions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Limitations and Drawbacks&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s briefly discuss some limitations and drawbacks of Lambda functions in Python:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Readability challenges in complex expressions&lt;/strong&gt;: While Lambda functions are concise, they can become difficult to read and understand when used for complex expressions. This can lead to code that is harder to maintain and debug.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limitations in error handling and debugging&lt;/strong&gt;: As Lambda functions can only contain a single expression, they can&amp;#39;t include statements, like the &lt;code&gt;try-except&lt;/code&gt; block for error handling. This limitation makes them unsuitable for complex operations that require these features.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restricted functionality&lt;/strong&gt;: Since Lambda functions can only contain a single expression, they are less versatile than standard functions. This by-design restriction limits their use to simple operations and transformations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Best Practices for Using Lambda Functions&lt;/h2&gt;
&lt;p&gt;Now that we&amp;#39;ve considered some pros and cons, let&amp;#39;s define some best practices for using Lambda functions effectively:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep them simple&lt;/strong&gt;: To maintain readability and simplicity, Lambda functions should be kept short and limited to straightforward operations. Functions with complex logic should be refactored into standard functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid overuse&lt;/strong&gt;: While Lambda functions are convenient for numerous situations, overusing them can lead to code that is difficult to read and maintain. Use them judiciously and opt for standard functions when clarity is fundamental.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combine Lambda functions with other Python features&lt;/strong&gt;: As we&amp;#39;ve seen, Lambda functions can be effectively combined with other Python features, such as list comprehensions and higher-order functions. This can result in more expressive and concise code when used appropriately.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Advanced Techniques with Lambda Functions&lt;/h2&gt;
&lt;p&gt;In certain cases, more advanced Lambda function techniques can be of help.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s see some examples.&lt;/p&gt;
&lt;h3&gt;Nested Lambda Functions&lt;/h3&gt;
&lt;p&gt;Lambda functions can be nested for complex operations.&lt;/p&gt;
&lt;p&gt;This technique is useful in scenarios where you need to have multiple small transformations in a sequence.&lt;/p&gt;
&lt;p&gt;For example, suppose you want to create a function that calculates the square root of a number and then adds 1. Here&amp;#39;s how you can use Lambda functions to do so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Create a nested lambda function
nested_lambda = lambda x: (lambda y: y ** 2)(x) + 1

# Print the result for the value 3
print(nested_lambda(3))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Integration with Python Libraries for Advanced Functionality&lt;/h3&gt;
&lt;p&gt;Many Python libraries leverage Lambda functions to simplify complex data processing tasks.&lt;/p&gt;
&lt;p&gt;For example, Lambda functions can be used with &lt;code&gt;Pandas&lt;/code&gt; and &lt;code&gt;NumPy&lt;/code&gt; to simplify data manipulation and transformation.&lt;/p&gt;
&lt;p&gt;Suppose we have a data frame with two columns. We want to create another column that is the sum of the other two. In this case, we can use Lambda functions as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Create the columns&amp;#39; data
data = {&amp;#39;A&amp;#39;: [1, 2, 3], &amp;#39;B&amp;#39;: [4, 5, 6]}

# Create data frame
df = pd.DataFrame(data)

# Create row C as A+B and print the dataframe
df[&amp;#39;C&amp;#39;] = df.apply(lambda row: row[&amp;#39;A&amp;#39;] + row[&amp;#39;B&amp;#39;], axis=1)
print(df)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;   A  B  C
0  1  4  5
1  2  5  7
2  3  6  9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s it for our whistle-stop tour of Lambda functions in Python!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we&amp;#39;ve seen how to use Lambda functions in Python, explored their pros and cons, some best practices, and touched on a couple of advanced use cases.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Integrating Open edX with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/10/02/integrating-open-edx-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2024/10/02/integrating-open-edx-with-appsignal.html</id>
    <published>2024-10-02T00:00:00+00:00</published>
    <updated>2024-10-02T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s see how we can use AppSignal to monitor and improve performance in an Open edX application.</summary>
    <content type="html">&lt;p&gt;Imagine stepping into the role of a DevOps engineer at an online learning company that utilizes Open edX as its core Learning Management System (LMS). As the platform scales to accommodate more learners, a myriad of challenges begin to surface:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frequent reports of sluggish platform responses&lt;/li&gt;
&lt;li&gt;Issues with password reset emails&lt;/li&gt;
&lt;li&gt;Discrepancies in grading reflected on progress pages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are just the tip of the iceberg. It&amp;#39;s pivotal that you provide timely reports on site performance and error tracking in real time, and fix any issues before they affect a significant user base.&lt;/p&gt;
&lt;p&gt;In this article (part one of a two-part series), we will see how AppSignal can be harnessed to enhance the robustness of Open edX. Leveraging AppSignal&amp;#39;s comprehensive suite of features, we aim to not only monitor — but also significantly improve — platform performance.&lt;/p&gt;
&lt;h3&gt;What is Open edX?&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://openedx.org/&quot;&gt;Open edX&lt;/a&gt; is at the forefront of the e-learning landscape, powering Learning Management Systems for millions worldwide. With a high demand for online education, maintaining a high-quality learning experience becomes increasingly crucial.&lt;/p&gt;
&lt;h2&gt;Why AppSignal for Open edX?&lt;/h2&gt;
&lt;p&gt;In such a dynamic environment, robust monitoring and error tracking are indispensable. This is where AppSignal shines, offering a powerful toolset for early issue detection, thereby mitigating potential impacts on your learners. By integrating AppSignal with Open edX, you unlock essential features for &lt;strong&gt;real-time performance monitoring&lt;/strong&gt;, &lt;strong&gt;automated error tracking&lt;/strong&gt;, and &lt;strong&gt;actionable insights&lt;/strong&gt; that drive informed decisions for your platform’s evolution.&lt;/p&gt;
&lt;p&gt;Throughout this article, we’ll explore practical strategies to leverage AppSignal to build a resilient, fail-safe learning platform that consistently delivers outstanding educational experiences.&lt;/p&gt;
&lt;h3&gt;Open edX Challenges&lt;/h3&gt;
&lt;p&gt;Over the last decade of working with Open edX, I&amp;#39;ve frequently heard the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;The site is slow.&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;We expect a high spike in enrollments and need more server resources.&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Users apply for password resets but never receive the emails.&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Grading delays are affecting the student progress page.&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;The site styling breaks intermittently.&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Students encounter 500 errors without any clear explanation.&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These issues underscore the critical need for a robust monitoring platform. Without it, functions like course enrollments could malfunction, overwhelming the support team or deteriorating user experiences. This may potentially &lt;strong&gt;cause learners to abandon courses&lt;/strong&gt; that educators have heavily invested in developing.&lt;/p&gt;
&lt;h3&gt;The Need for Dedicated Monitoring&lt;/h3&gt;
&lt;p&gt;Open edX lacks a default system for real-time error tracking, performance monitoring, and user behavior analysis. This gap necessitates a third-party solution that can integrate seamlessly and enhance platform reliability.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Concurrent Users and Performance Issues&lt;/strong&gt;: Handling large numbers of concurrent users during busy periods is vital for ensuring a smooth, uninterrupted user experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uptime Monitoring&lt;/strong&gt;: Monitoring downtimes and uptimes is crucial to maintaining a consistent learning environment, and minimizing student frustration due to technical issues.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anomaly Detection and Error Tracking&lt;/strong&gt;: Effective monitoring includes identifying unusual behaviors or patterns in user interactions, and providing detailed insights that help to improve educational content and methodologies.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While tools like Sentry and DataDog offer valuable features, they may not fully meet the unique demands of an e-learning environment. This brings us to the distinct advantages of AppSignal.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can visit &lt;a href=&quot;https://www.appsignal.com/alternative/datadog-alternative&quot;&gt;Compare AppSignal to Datadog&lt;/a&gt; and &lt;a href=&quot;https://www.appsignal.com/alternative/new-relic-alternative&quot;&gt;Compare AppSignal to New Relic&lt;/a&gt; to learn about AppSignal&amp;#39;s difference to these tools and why it is a better decision to pick AppSignal for Open edX.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;How does AppSignal Benefit Open edX?&lt;/h3&gt;
&lt;p&gt;AppSignal stands out by catering specifically to the needs of platforms like Open edX, providing a robust set of tools designed to tackle the unique challenges of online learning environments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;All-Inclusive Features&lt;/strong&gt;: AppSignal offers a &lt;strong&gt;comprehensive suite of monitoring tools&lt;/strong&gt;, including error tracking, performance monitoring, and anomaly detection. This holistic approach eliminates the need to manage multiple services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-Time Analytics and Dashboards&lt;/strong&gt;: With customizable dashboards, AppSignal provides immediate insights into critical metrics such as error rates, uptime, and response times. You can also create custom metrics like &lt;strong&gt;enrollment count&lt;/strong&gt; and visualize these metrics via a custom dashboard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ease of Integration&lt;/strong&gt;: The AppSignal SDK makes integration straightforward, minimizing setup time and allowing for quick implementation of comprehensive monitoring capabilities.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Incorporating AppSignal not only addresses the immediate operational challenges but also enhances the strategic development of Open edX platforms by providing deep, actionable insights into system performance and user engagement.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AppSignal uses the &lt;a href=&quot;https://opentelemetry.io/&quot;&gt;OpenTelemetry&lt;/a&gt; protocol, which provides a robust foundation for observability. This integration brings a wealth of out-of-the-box tools and features, enhancing the monitoring capabilities of your Open edX platform.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;To integrate AppSignal with Open edX, we need the following requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python 3.8 or higher&lt;/li&gt;
&lt;li&gt;Django 1.10 or higher&lt;/li&gt;
&lt;li&gt;Docker v24.0.5+ (with BuildKit 0.11+)&lt;/li&gt;
&lt;li&gt;Docker Compose v2.0.0+&lt;/li&gt;
&lt;li&gt;Open edX &lt;a href=&quot;https://docs.openedx.org/en/latest/community/release_notes/index.html&quot;&gt;Koa version&lt;/a&gt; or higher&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://appsignal.com/users/sign_up&quot;&gt;An AppSignal account — sign up for a free 30-day trial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Basic Django and Open edX knowledge&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this article, we will use &lt;a href=&quot;https://docs.tutor.edly.io/install.html&quot;&gt;Tutor&lt;/a&gt; to install and customize Open edX.&lt;/p&gt;
&lt;h3&gt;How to Install Open edX&lt;/h3&gt;
&lt;p&gt;In this section, we&amp;#39;ll quickly review how to install the Open edX Dev instance on the Ubuntu 22.04 operating system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To Install Tutor on other operating systems, please visit the &lt;a href=&quot;https://docs.tutor.edly.io/install.html&quot;&gt;official Tutor docs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Open your terminal and run the following commands (before continuing, make sure you have the necessary libraries and applications mentioned in the Prerequisites section):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update -y
sudo apt upgrade -y
sudo apt install python3 python3-pip libyaml-dev
pip install &amp;quot;tutor[full]&amp;quot;
tutor local launch
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Provide all the necessary information and a correct URL pointing to your server&amp;#39;s IP address.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After finishing the deployment you should be able to visit the URL you provided to see your Open edX instance.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/openedx.png&quot; alt=&quot;openedx&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Integrate AppSignal&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s integrate Open edX with AppSignal to start receiving data in our AppSignal Dashboard.&lt;/p&gt;
&lt;p&gt;First, go to the &lt;a href=&quot;https://appsignal.com/users/sign_in&quot;&gt;AppSignal login page&lt;/a&gt;, create a new Python application, and grab your &lt;strong&gt;API KEY&lt;/strong&gt;. We are going to need that in the next step.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/install_appsignal.png&quot; alt=&quot;Install Appsignal&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s go to where you installed Tutor. Open the file &lt;code&gt;.local/share/tutor/config.yml&lt;/code&gt; and add the following libraries to the &lt;code&gt;OPENEDX_EXTRA_PIP_REQUIREMENTS&lt;/code&gt; configuration to install all the necessary libraries for AppSignal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;OPENEDX_EXTRA_PIP_REQUIREMENTS:
  - appsignal==1.3.0
  - opentelemetry-instrumentation-django==0.45b0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, run &lt;code&gt;tutor config save&lt;/code&gt; and relaunch the platform by running &lt;code&gt;tutor local launch&lt;/code&gt;. This should install all the necessary packages for you.&lt;/p&gt;
&lt;p&gt;After the deployment completes, we&amp;#39;ll initialize AppSignal in our Open edX code base and recreate the Open edX Docker images.&lt;/p&gt;
&lt;p&gt;Clone the &lt;a href=&quot;https://github.com/openedx/edx-platform/tree/open-release/quince.master&quot;&gt;Quince branch&lt;/a&gt; of the &lt;code&gt;edx-platform&lt;/code&gt; codebase: &lt;code&gt;git clone https://github.com/openedx/edx-platform/tree/open-release/quince.master&lt;/code&gt;. Open &lt;code&gt;edx-platform&lt;/code&gt; in your favorite editor and follow these instructions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the &lt;code&gt;lms&lt;/code&gt; directory, create a new file called &lt;code&gt;__appsignal__.py&lt;/code&gt; and add the following code:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import os
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration

# Get APPSIGNAL_PUSH_API_KEY from environment
site = SiteConfiguration.objects.get(site__domain=&amp;quot;thelearningalgorithm.ai&amp;quot;)
APPSIGNAL_PUSH_API_KEY = os.environ.get(&amp;quot;APPSIGNAL_PUSH_API_KEY&amp;quot;)
os.environ.setdefault(&amp;quot;PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION&amp;quot;, &amp;quot;python&amp;quot;)

from appsignal import Appsignal
appsignal = Appsignal(
    active=True,
    name=&amp;quot;openedx&amp;quot;,
    push_api_key=APPSIGNAL_PUSH_API_KEY,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;In your tutor server, create a new environment variable called &lt;code&gt;APPSIGNAL_PUSH_API_KEY&lt;/code&gt; and set the AppSignal API key there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Next, let&amp;#39;s start AppSignal in &lt;code&gt;wsgi&lt;/code&gt;. Go to &lt;code&gt;lms/wsgi.py&lt;/code&gt; and add the following code:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;modulestore() # exiting line

# import appsignal
from lms.__appsignal__ import appsignal

# Start Appsignal
appsignal.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can look at &lt;a href=&quot;https://github.com/amirtds/edx-platform/commit/3347a2bc65a9142c382ef0c1b00906d7ea654ce4&quot;&gt;this commit&lt;/a&gt; to see how I did it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/django.html&quot;&gt;Learn more about integrating AppSignal with Django&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, create a new repo for your &lt;code&gt;edx-platform&lt;/code&gt; on GitHub and push your changes there as a new branch. When you are done, run the following command to build a new image for the Open edX container based on your changes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tutor images build openedx --build-arg EDX_PLATFORM_REPOSITORY=https://github.com/[YOUR ORGANIZTION]/edx-platform.git --build-arg EDX_PLATFORM_VERSION=[YOUR BRANCH NAME] &amp;amp;&amp;amp; tutor local stop &amp;amp;&amp;amp; tutor local start -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to build based on my branch, you can run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tutor images build openedx --build-arg EDX_PLATFORM_REPOSITORY=https://github.com/amirtds/edx-platform.git --build-arg EDX_PLATFORM_VERSION=feat/appsignal-integration &amp;amp;&amp;amp; tutor local stop &amp;amp;&amp;amp; tutor local start -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s it! Now go to your Open edX site and do some basic interactions like logging in and viewing course content. You should see that your AppSignal Dashboard starts populating with data from your instance.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In this section, we added AppSignal for the LMS part of Open edX. You can follow the same steps in the CMS directory to add monitoring for the studio.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Track Errors with AppSignal&lt;/h2&gt;
&lt;p&gt;Now head to the AppSignal Dashboard and click on &lt;strong&gt;Errors -&amp;gt; Issue List&lt;/strong&gt; on the left sidebar. Here, you should see all the errors that occur when users visit specific URLs. By clicking on each issue, you should see more detailed information.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Integrate AppSignal with your GitHub repo so that an issue is created in the repo automatically when an error occurs. This way, you won&amp;#39;t lose track of errors.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/errors_list.png&quot; alt=&quot;Errors list&quot;/&gt;&lt;/p&gt;
&lt;p&gt;In the &lt;strong&gt;Graphs&lt;/strong&gt;, we can see the error rate, count, and throughput:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/errors_graphs.png&quot; alt=&quot;errors graph&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Monitor Performance&lt;/h2&gt;
&lt;p&gt;In the &lt;strong&gt;Performance&lt;/strong&gt; section, click on the &lt;strong&gt;Issues&lt;/strong&gt; list. Here, you should see a breakdown of all the visited URLs and important metrics, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mean:&lt;/strong&gt; The average response time for all the requests made to a particular endpoint. It provides a general idea of how long it takes for the server to respond. In our context, any mean response time greater than 1 second could be considered a red flag, indicating that our application&amp;#39;s performance might not meet user expectations for speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput:&lt;/strong&gt; This is the number of requests that are being handled per second.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Impact:&lt;/strong&gt; An action&amp;#39;s impact on our application, based on its usage compared to other actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/performance_issue_list.png&quot; alt=&quot;performance list&quot;/&gt;&lt;/p&gt;
&lt;p&gt;We can also see graphs showing our application&amp;#39;s performance:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/performance_graphs.png&quot; alt=&quot;performance graphs&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This information is highly important for us since it shows the user experience when loading each page.
When numbers are high, we should look for ways to improve the user experience and reduce the mean response time. For example, in our case, we have multiple requests that take longer than 1 second. That means there is room to improve our application&amp;#39;s performance.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/long_requests.png&quot; alt=&quot;performance long requests&quot;/&gt;&lt;/p&gt;
&lt;p&gt;In the &lt;strong&gt;Performance&lt;/strong&gt; section, we can also monitor background tasks and queries like Celery tasks. We&amp;#39;ll look at this in more depth in part two of this series.&lt;/p&gt;
&lt;h2&gt;Anomaly Detection and Alerts&lt;/h2&gt;
&lt;p&gt;Here, we can define triggers when there is unusual resource usage on our platform. It is highly important to monitor resource usage and take action so our site doesn&amp;#39;t go down when there&amp;#39;s a spike in users. Additionally, an important function on the platform might fail, resulting in a high number of errors. By using anomaly detection, we get a notification before it impacts more users.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s create 2 triggers: one for &lt;strong&gt;high memory usage&lt;/strong&gt; when it&amp;#39;s above &lt;em&gt;70%&lt;/em&gt; and one for &lt;strong&gt;% of error rates&lt;/strong&gt; when they are higher than &lt;em&gt;20%&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In the &lt;strong&gt;Anomaly Detection&lt;/strong&gt; section on the left sidebar, click on &lt;strong&gt;Triggers&lt;/strong&gt; and follow these steps:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/anomaly_trigger_1.png&quot; alt=&quot;anomaly trigger 1&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/anomaly_trigger_2.png&quot; alt=&quot;anomaly trigger 2&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/anomaly_triggers.png&quot; alt=&quot;anomaly trigger all&quot;/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Check the &lt;strong&gt;Email notification&lt;/strong&gt; to receive an email notification when an anomaly happens.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the &lt;strong&gt;Issues&lt;/strong&gt; list, you can see all anomalies based on defined conditions:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/anomaly_happened.png&quot; alt=&quot;anomaly trigger happened&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Uptime Monitoring&lt;/h2&gt;
&lt;p&gt;In my experience with Open edX over the past few years, two scenarios have consistently highlighted the need for robust uptime monitoring:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Post-Deployment Styling Issues&lt;/strong&gt;: After deployments, CSS links can sometimes change, leading to a 404 error on the CSS resource URL and a broken appearance due to missing styles. This not only impacts the aesthetic appeal of Open edX, but can also confuse and frustrate users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System Downtimes&lt;/strong&gt;: Various factors can bring an entire site down, such as database outages or failures in the third-party systems that we&amp;#39;ve integrated. These downtimes disrupt the learning process and can damage our platform&amp;#39;s reputation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Uptime monitoring is invaluable as it not only confirms our site&amp;#39;s operational status, but also alerts us the moment it goes down, allowing us to take quick action to restore functionality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Regional Response Times&lt;/strong&gt;: An interesting feature of AppSignal&amp;#39;s uptime check is its ability to display response times from different regions. This data provides critical insights into how a site&amp;#39;s performance varies globally, which is particularly useful for international educational platforms looking to deliver a consistent user experience across diverse geographical locations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Public Status Page&lt;/strong&gt;: You can set up a public status page to communicate real-time site status. This page serves as a reliable source for students to verify whether a site is operational, and can reduce the number of support tickets you get.
For instance, you can &lt;a href=&quot;https://lms.appsignal-status.com/&quot;&gt;view an example here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/uptime_public_page.png&quot; alt=&quot;uptimecheck public page&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Simply add a new URL by clicking on &lt;strong&gt;Add Uptime Monitor&lt;/strong&gt;. In this case, I created one for the CSS resource and one for the main LMS URL.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/uptime_check_1.png&quot; alt=&quot;uptimecheck&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/uptime_check_2.png&quot; alt=&quot;uptimecheck&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Host Monitoring&lt;/h2&gt;
&lt;p&gt;Here, we see how our VM resources are being used by our application through visuals and graphs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-10/host_metric_1.png&quot; alt=&quot;host_metric_1&quot;/&gt;
&lt;img src=&quot;/images/blog/2024-10/host_metric_2.png&quot; alt=&quot;host_metric_2&quot;/&gt;
&lt;img src=&quot;/images/blog/2024-10/host_metric_3.png&quot; alt=&quot;host_metric_3&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we&amp;#39;ve explored how AppSignal can be integrated with Open edX to bring robust monitoring capabilities to your learning platform. Without requiring extensive configuration beyond an initial setup, AppSignal provides a rich array of features right out of the box. These features enable real-time insights into application performance, error tracking, and uptime monitoring — all essential tools for any DevOps team dedicated to creating a fail-safe learning environment.&lt;/p&gt;
&lt;p&gt;In part two of this two-part series, we will dive deeper into the capabilities of AppSignal by demonstrating how to stream logs from Open edX to AppSignal, monitor background tasks with Celery, track Redis queries, and more.&lt;/p&gt;
&lt;p&gt;Until then, happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Integrating Stripe Into A One-Product Django Python Shop</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/09/04/integrating-stripe-into-a-one-product-django-python-shop.html"/>
    <id>https://blog.appsignal.com/2024/09/04/integrating-stripe-into-a-one-product-django-python-shop.html</id>
    <published>2024-09-04T00:00:00+00:00</published>
    <updated>2024-09-04T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s handle orders on our one-product shop by setting up Stripe.</summary>
    <content type="html">&lt;p&gt;In the first part of this series, we created a Django online shop with htmx.&lt;/p&gt;
&lt;p&gt;In this second part, we&amp;#39;ll handle orders using Stripe.&lt;/p&gt;
&lt;h2&gt;What We&amp;#39;ll Do&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll integrate Stripe to handle payments securely. This is what we want to achieve:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In the &lt;code&gt;purchase&lt;/code&gt; view, we start by creating a Stripe checkout session and redirect the customer to the corresponding URL. This is where we tell Stripe about the product we are selling, its quantity, and where the customer should be redirected to after a successful purchase (the &lt;code&gt;success_url&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The customer fills in their payment details on the Stripe checkout page and completes the payment. Stripe then makes a POST request to a webhook endpoint on our website, where we listen to events and process them accordingly. If the payment is successful, we save the order in our database and notify the customer (and our staff users) about the purchase.&lt;/li&gt;
&lt;li&gt;Finally, if the webhook returns a response with a 200 OK HTTP status code, Stripe redirects to the &lt;code&gt;success_url&lt;/code&gt; created in the first step.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Setting Up Stripe for Our Django Python Store&lt;/h2&gt;
&lt;p&gt;We first need to jump over to &lt;a href=&quot;https://stripe.com/&quot;&gt;Stripe&lt;/a&gt; and do the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://dashboard.stripe.com/register&quot;&gt;Create a Stripe account&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dashboard.stripe.com/test/products&quot;&gt;Create a product (with a payment id)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dashboard.stripe.com/test/webhooks&quot;&gt;Create a webhook&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1: Create a Stripe Account&lt;/h3&gt;
&lt;p&gt;Start by &lt;a href=&quot;https://dashboard.stripe.com/register&quot;&gt;creating a Stripe account&lt;/a&gt;. For now, you don’t really need to activate your account. You can just work in test mode, which will prevent you from making real payments while testing. Go to the &lt;a href=&quot;https://dashboard.stripe.com/test/apikeys&quot;&gt;API keys page&lt;/a&gt; and retrieve the publishable and secret keys. Save them in your project environment variables (&lt;code&gt;STRIPE_PUBLISHABLE_KEY&lt;/code&gt; and &lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt;). We will use these keys to authenticate your Stripe requests.&lt;/p&gt;
&lt;h3&gt;2: Create Your Product&lt;/h3&gt;
&lt;p&gt;Create a new product on the &lt;a href=&quot;https://dashboard.stripe.com/test/products&quot;&gt;products page&lt;/a&gt;. Fill out the details and set the payment type to &lt;em&gt;one-off&lt;/em&gt;. Your product should look something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-09/create-product-stripe.png&quot; alt=&quot;Create product on Stripe&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Once you press &lt;strong&gt;Add product&lt;/strong&gt;, you should be able to see your product on the product list. If you click on it and scroll down to the &lt;strong&gt;Pricing&lt;/strong&gt; section, you can find the &lt;strong&gt;API ID&lt;/strong&gt; for the price item you created — it should be something like &lt;code&gt;price_3ODP5…&lt;/code&gt;. Save it in an environment variable (&lt;code&gt;STRIPE_PRICE_ID&lt;/code&gt;): you will need this when creating the Stripe checkout session.&lt;/p&gt;
&lt;h3&gt;3: Create the Webhook&lt;/h3&gt;
&lt;p&gt;We need to create a webhook endpoint for Stripe to call when a payment completes. In the &lt;a href=&quot;https://dashboard.stripe.com/test/webhooks&quot;&gt;webhooks page&lt;/a&gt;, choose to test in the local environment. This will allow you to forward the request to a local URL, like &lt;a href=&quot;http://127.0.0.1:8000/&quot;&gt;http://127.0.0.1:8000&lt;/a&gt;. Start by &lt;a href=&quot;https://docs.stripe.com/stripe-cli&quot;&gt;downloading the Stripe CLI&lt;/a&gt;. Then, you can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log into Stripe&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe login
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Forward events to the webhook endpoint that you will create:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe listen --forward-to http://127.0.0.1:8000/webhook
&amp;gt; Ready! Your webhook signing secret is whsec_06531a7ba22363ac038f284ac547906b89e5c939f8d55dfd03a3619f9adc590a (^C to quit)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures that once a purchase is made, Stripe forwards the webhook calls to your local endpoint. The command will log a webhook signing secret, which you should also save as a project environment variable (&lt;code&gt;STRIPE_WEBHOOK_SECRET&lt;/code&gt;). This will prove useful for verifying that a request does indeed come from Stripe and that you are &lt;a href=&quot;https://docs.stripe.com/webhooks/quickstart#signature-verify&quot;&gt;handling the right webhook&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By the end of this section, you should have four Stripe environment variables. You can now load them in &lt;code&gt;ecommerce_site/settings.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce_site/settings.py

import os
from dotenv import load_dotenv
load_dotenv()

STRIPE_PUBLISHABLE_KEY = os.environ.get(&amp;quot;STRIPE_PUBLISHABLE_KEY&amp;quot;)
STRIPE_SECRET_KEY = os.environ.get(&amp;quot;STRIPE_SECRET_KEY&amp;quot;)
STRIPE_PRICE_ID = os.environ.get(&amp;quot;STRIPE_PRICE_ID&amp;quot;)
STRIPE_WEBHOOK_SECRET = os.environ.get(&amp;quot;STRIPE_WEBHOOK_SECRET&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: We are using &lt;a href=&quot;https://pypi.org/project/python-dotenv/&quot;&gt;python-dotenv&lt;/a&gt; to load the environment variables.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Extend the Views&lt;/h2&gt;
&lt;p&gt;We now need to extend the views to integrate Stripe by creating a checkout session, a successful purchase view, and a webhook view.&lt;/p&gt;
&lt;h3&gt;1: Create a Stripe Checkout Session&lt;/h3&gt;
&lt;p&gt;In the &lt;code&gt;purchase&lt;/code&gt; view, we&amp;#39;ll create a Stripe checkout session if the purchase form is valid:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce/views.py
from django_htmx import HttpResponseClientRedirect
from django.conf import settings
import stripe

@require_POST
def purchase(request):
    form = OrderForm(request.POST)
    if form.is_valid():
        quantity = form.cleaned_data[&amp;quot;quantity&amp;quot;]

        # replace time.sleep(2) with the following code ⬇️

        # 1 - set stripe api key
        stripe.api_key = settings.STRIPE_SECRET_KEY

        # 2 - create success url
        success_url = (
            request.build_absolute_uri(
                reverse(&amp;quot;purchase_success&amp;quot;)
            )
            + &amp;quot;?session_id={CHECKOUT_SESSION_ID}&amp;quot;
        )

        # 3 - create cancel url
        cancel_url = request.build_absolute_uri(reverse(&amp;quot;home&amp;quot;))

        # 4 - create checkout session
        checkout_session = stripe.checkout.Session.create(
            line_items=[
                {
                    &amp;quot;price&amp;quot;: settings.STRIPE_PRICE_ID,
                    &amp;quot;quantity&amp;quot;: quantity,
                }
            ],
            mode=&amp;quot;payment&amp;quot;,
            success_url=success_url,
            cancel_url=cancel_url
        )

        # 5 - redirect to checkout session url
        return HttpResponseClientRedirect(checkout_session.url)
    return render(request, &amp;quot;product.html&amp;quot;, {&amp;quot;form&amp;quot;: form})
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;Let’s break this down:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We first set the Stripe API key.&lt;/li&gt;
&lt;li&gt;We then create a successful purchase URL pointing to the &lt;code&gt;purchase_success&lt;/code&gt; view (which we&amp;#39;ll create in the next step). Stripe should automatically populate the &lt;code&gt;CHECKOUT_SESSION_ID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We create a URL for when a purchase is canceled — for example, when the customer changes their mind. In this case, it’s just the home view.&lt;/li&gt;
&lt;li&gt;We create a Stripe checkout session with our price ID (the product identifier) and the quantity the customer wants to purchase.&lt;/li&gt;
&lt;li&gt;Stripe returns a session object from which we can extract the URL and redirect the customer. Since this request is coming from htmx, we can’t really use the standard Django redirect function. Instead, we use the &lt;a href=&quot;https://django-htmx.readthedocs.io/&quot;&gt;django-htmx&lt;/a&gt; package, which provides this &lt;code&gt;HttpResponseClientRedirect&lt;/code&gt; class.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2: Create the Successful Purchase View&lt;/h3&gt;
&lt;p&gt;After completing the purchase, Stripe will redirect the customer to our specified &lt;code&gt;success_url&lt;/code&gt;. Here, we can handle the post-purchase logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.shortcuts import redirect

def purchase_success(request):
    session_id = request.GET.get(&amp;quot;session_id&amp;quot;)
    if session_id is None:
          return redirect(&amp;quot;home&amp;quot;)

    stripe.api_key = settings.STRIPE_SECRET_KEY
    try:
        stripe.checkout.Session.retrieve(session_id)
    except stripe.error.InvalidRequestError:
        messages.error(request, &amp;quot;There was a problem while buying your product. Please try again.&amp;quot;)
        return redirect(&amp;quot;home&amp;quot;)
    return render(request, &amp;quot;purchase_success.html&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this view, we first check if the &lt;code&gt;session_id&lt;/code&gt; query parameter is present. If it is, we retrieve the corresponding session from Stripe using the secret key and the &lt;code&gt;session_id&lt;/code&gt;. We then render the successful purchase template, which looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;# ecommerce/templates/purchase_success.html {% extends &amp;quot;base.html&amp;quot; %} {% block
content %}
&amp;lt;section&amp;gt;
  &amp;lt;header&amp;gt;
    &amp;lt;h2&amp;gt;Thank you for your purchase&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;
      Your purchase was successful. You will receive an email with the details
      of your purchase soon.
    &amp;lt;/p&amp;gt;
  &amp;lt;/header&amp;gt;
&amp;lt;/section&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should also add it to the &lt;code&gt;urlpatterns&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce_site/urls.py

# ... same imports as before

urlpatterns = [
    # ... same urls as before
    path(&amp;quot;purchase_success&amp;quot;, views.purchase_success, name=&amp;quot;purchase_success&amp;quot;),  # ⬅️ new
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3: Create the Webhook View&lt;/h3&gt;
&lt;p&gt;While the customer is in the purchase process, and before they are redirected to the success view, Stripe will call our webhook endpoint (remember to have the webhook listener running, as explained in the earlier &amp;#39;Create the Webhook&amp;#39; section of this post):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse

@csrf_exempt
def webhook(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY
    sig_header = request.headers.get(&amp;#39;stripe-signature&amp;#39;)
    payload = request.body
    event = None
    try:
            event = stripe.Webhook.construct_event(
                payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
            )
    except stripe.error.SignatureVerificationError:
        # Invalid signature
        return HttpResponse(status=400)

    # Handle the checkout.session.completed event
    if event.type == &amp;quot;checkout.session.completed&amp;quot;:
        # TODO: create line orders
        return HttpResponse(status=200)
    return HttpResponse(status=400)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s break this down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We try to construct a Stripe event from the payload, the signature header, and the webhook secret: the first is used to build the actual event, and the last two variables are relevant to validate the authenticity of the request.&lt;/li&gt;
&lt;li&gt;If the signature verification fails, we return a 400 HTTP response. Remember that Stripe is actually calling this endpoint, not our customer, so Stripe will know what to do in this scenario.&lt;/li&gt;
&lt;li&gt;We check if the event type is &lt;code&gt;checkout.session.completed&lt;/code&gt;, i.e., if a customer successfully paid for our product. For now, we don’t do much else here, but we will process the order in the next step.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; A Stripe event can have multiple types but we will only handle completed sessions in this post. However, you can (and should) &lt;a href=&quot;https://docs.stripe.com/webhooks&quot;&gt;extend a webhook by following the docs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You should also add this view to &lt;code&gt;urlpatterns&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce_site/urls.py

# ... same imports as before

urlpatterns = [
    # ... same urls as before
    path(&amp;quot;webhook&amp;quot;, views.webhook, name=&amp;quot;webhook&amp;quot;),  # ⬅️ new
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything works well, once you click “buy”, you should be redirected to a Stripe payment page. Since we are in test mode, we can &lt;a href=&quot;https://docs.stripe.com/testing?testing-method=card-numbers&quot;&gt;fill in the payment details with dummy data&lt;/a&gt;, like a &lt;code&gt;4242 4242 4242 4242&lt;/code&gt; card:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-09/buy-product-stripe.png&quot; alt=&quot;buy product on Stripe&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Once you press &lt;strong&gt;Pay&lt;/strong&gt;, Stripe should call the webhook view and redirect you to the &lt;code&gt;purchase_success&lt;/code&gt; view. Congratulations, you have successfully processed a payment with Stripe!&lt;/p&gt;
&lt;h2&gt;Create the Orders and Notify Users&lt;/h2&gt;
&lt;p&gt;Once a purchase is completed, we need to do a few things in the webhook view:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Save the order information in our database.&lt;/li&gt;
&lt;li&gt;Notify staff users about the recent purchase.&lt;/li&gt;
&lt;li&gt;Send a confirmation email to the customer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s create a &lt;code&gt;LineOrder&lt;/code&gt; database model in &lt;code&gt;ecommerce/models.py&lt;/code&gt; to store some of the order information:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce/models.py

from django.db import models

class LineOrder(models.Model):
    quantity = models.IntegerField()
    name = models.CharField(max_length=255, null=True, blank=True)
    email = models.EmailField(null=True, blank=True)
    shipping_details = models.TextField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f&amp;quot;Order {self.id} - {self.quantity} units&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember to create and run the migrations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py makemigrations # ⬅️ creates the migration files
python manage.py migrate # ⬅️ applies the migrations in the database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can now create a function to process the orders and call it from the webhook view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce/views.py

@csrf_exempt
def webhook(request):
    # ...same code as before
        if event.type == &amp;quot;checkout.session.completed&amp;quot;:
            create_line_orders(event.data.object) # ⬅️ new
            return HttpResponse(status=200)
        return HttpResponse(status=400)

# new ⬇️
def create_line_orders(session: stripe.checkout.Session):
    line_items = stripe.checkout.Session.list_line_items(session.id)
    for line_item in line_items.data:
        LineOrder.objects.create(
            name=session.customer_details.name,
            email=session.customer_details.email,
            shipping_details=session.shipping_details,
            quantity=line_item.quantity,
        )
    mail.send_mail(
        &amp;quot;Your order has been placed&amp;quot;,
        f&amp;quot;&amp;quot;&amp;quot;
        Hi {session.customer_details.name},
        Your order has been placed. Thank you for shopping with us!
        You will receive an email with tracking information shortly.

        Best,
        The one product e-commerce Team
        &amp;quot;&amp;quot;&amp;quot;,
        &amp;quot;from@example.com&amp;quot;,
        [session.customer_details.email],
    )

    staff_users = User.objects.filter(is_staff=True)
    mail.send_mail(
        &amp;quot;You have a new order!&amp;quot;,
        &amp;quot;&amp;quot;&amp;quot;
            Hi team!
            You have a new order in your shop! go to the admin page to see it.

            Best,
            The one product e-commerce Team
            &amp;quot;&amp;quot;&amp;quot;,
        &amp;quot;from@example.com&amp;quot;,
        [user.email for user in staff_users],
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s break this down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We first create line order instances from the Stripe session and send a confirmation email to the customer about their purchase.&lt;/li&gt;
&lt;li&gt;We then send an email to all staff users telling them to check the admin panel.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can now register the &lt;code&gt;LineOrder&lt;/code&gt; model in the admin panel, so it’s accessible to staff users:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce/admin.py
from django.contrib import admin
from ecommerce.models import LineOrder

# Register your models here.
admin.site.register(LineOrder)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When staff users log in to the admin page, they will now be able to check new orders and process them accordingly — in this case, pack and ship mugs to the customer!&lt;/p&gt;
&lt;h2&gt;Some Tips to Optimize Your Django Store&lt;/h2&gt;
&lt;p&gt;Here are some tips to further improve on the store you&amp;#39;ve built:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write tests - you can see some &lt;a href=&quot;https://github.com/franciscobmacedo/ecommerce-site/blob/main/ecommerce/tests.py&quot;&gt;examples in the GitHub repository&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you have more products to sell, create a database model for them, and connect the &lt;code&gt;LineOrder&lt;/code&gt; through a &lt;code&gt;ForeignKey&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Configure email settings according to &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/topics/email/&quot;&gt;Django&amp;#39;s email documentation&lt;/a&gt;. You can also use libraries such as &lt;a href=&quot;https://pypi.org/project/django-post-office/&quot;&gt;django-post-office&lt;/a&gt; to manage your email templates and queues.&lt;/li&gt;
&lt;li&gt;Once you deploy your website, &lt;a href=&quot;https://dashboard.stripe.com/test/webhooks&quot;&gt;create an actual webhook (not a local listener)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Take a look at the Stripe docs for alternatives to the checkout process we&amp;#39;ve outlined, including an &lt;a href=&quot;https://docs.stripe.com/checkout/embedded/quickstart&quot;&gt;embedded checkout form&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this two-part series, we successfully built a one-product e-commerce site using Django, htmx, and Stripe. This guide has walked you through setting up your Django project, integrating htmx for seamless user interactions, and incorporating secure payments with Stripe.&lt;/p&gt;
&lt;p&gt;We also covered how to handle order processing, including saving order information to your database, notifying staff users of new purchases, and sending confirmation emails to your customers. With these foundations, you can further customize and expand your e-commerce site to suit your specific needs.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Build a One-Product Shop With the Python Django Framework and Htmx</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/08/28/build-a-one-product-shop-with-the-python-django-framework-and-htmx.html"/>
    <id>https://blog.appsignal.com/2024/08/28/build-a-one-product-shop-with-the-python-django-framework-and-htmx.html</id>
    <published>2024-08-28T00:00:00+00:00</published>
    <updated>2024-08-28T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll use Django, htmx, and Stripe to create a one-product e-commerce website.</summary>
    <content type="html">&lt;p&gt;This is the first of a two-part series using Django, htmx, and Stripe to create a one-product e-commerce website. In this part, we&amp;#39;ll start our Django project and integrate it with htmx.&lt;/p&gt;
&lt;p&gt;In the second part, we&amp;#39;ll handle the orders with Stripe.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get going!&lt;/p&gt;
&lt;h2&gt;Why Django, htmx, and Stripe?&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll be using Django, htmx, and Stripe to create our website because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Django&lt;/strong&gt; is a Python web framework with a great ORM, templating, and routing system. It has a few features straight out of the box (like authentication) and multiple open source packages available online. We will use Django to build the website.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;htmx&lt;/strong&gt; is a JavaScript library that gives your website a modern feel just by using html attributes — you don’t actually have to write any JavaScript (although you can, of course). We will use htmx to give some interactivity to our page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stripe&lt;/strong&gt; is a payments platform with a great API — it handles payments and credit card information. It also integrates nicely with Google and Apple Pay. We will use Stripe to facilitate product payments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s how the final product will work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A user goes to our website and is able to see some information about our product, including its price and description.&lt;/li&gt;
&lt;li&gt;Once the user clicks the “buy” button, they are redirected to Stripe’s checkout session.&lt;/li&gt;
&lt;li&gt;If the payment is successful, they are redirected to our website again. We save their order information and send a confirmation email to the customer and all staff users to notify them of the recent purchase.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now let&amp;#39;s configure our Django project, create the initial views, and build the purchase form with htmx.&lt;/p&gt;
&lt;h2&gt;Configure Your Django Python Project&lt;/h2&gt;
&lt;p&gt;To set up our project, we need to create a virtual environment, activate it, and install the required packages. We can then create our Django project and Django app.&lt;/p&gt;
&lt;h3&gt;Create a Virtual Environment&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s start by creating a virtual environment, so we can isolate our dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s how we activate it on Linux/Mac:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And on Windows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;.venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Install the Required Packages&lt;/h3&gt;
&lt;p&gt;Within our activated virtual environment, we need a few packages to make this work:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install django stripe django-htmx python-dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we have installed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.djangoproject.com/&quot;&gt;Django&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.stripe.com/api?lang=python&quot;&gt;Stripe&lt;/a&gt; to work with the stripe API.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://django-htmx.readthedocs.io/en/latest/&quot;&gt;django-htmx&lt;/a&gt;, which provides some helper functionality to work with htmx.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://saurabh-kumar.com/python-dotenv/&quot;&gt;python-dotenv&lt;/a&gt; to load environment variables from &lt;code&gt;.env&lt;/code&gt; files.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Create the Django Project&lt;/h3&gt;
&lt;p&gt;In the same directory as our virtual environment, let&amp;#39;s create a Django project called &lt;code&gt;ecommerce_site&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;django-admin startproject ecommerce_site .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Django, it&amp;#39;s good practice to have code organized by one or more &amp;quot;apps&amp;quot;. Each app is a package that does something in particular. A project can have multiple apps, but for this simple shop, we can just have one app that will have most of the code — the views, forms, and models for our e-commerce platform. Let&amp;#39;s create it and call it &lt;code&gt;ecommerce&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py startapp ecommerce
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And add this app to our &lt;code&gt;INSTALLED_APPS&lt;/code&gt; in &lt;code&gt;ecommerce_site/settings.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce_site/settings.py

INSTALLED_APPS = [
    # ... the default django apps
    &amp;quot;ecommerce&amp;quot;, # ⬅️ new
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you’re having trouble with this setup, &lt;a href=&quot;https://github.com/franciscobmacedo/ecommerce-site/blob/main/&quot;&gt;check out the final product&lt;/a&gt;. At this stage, your file structure should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ecommerce_site/
├── .venv/  # ⬅️ the virtual environment
├── ecommerce_site/ # ⬅️ the django project configuration
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── ecommerce/ # ⬅️ our app setup
│		├── templates/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── manage.py
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h3&gt;Create the Templates&lt;/h3&gt;
&lt;p&gt;Now that we have our project configured, we need to create some base layouts. In the templates directory, add a &lt;code&gt;base.html&lt;/code&gt; file — the template that all other templates will inherit from. Add &lt;a href=&quot;https://htmx.org/&quot;&gt;htmx&lt;/a&gt; for user interaction, &lt;a href=&quot;https://andybrewer.github.io/mvp/&quot;&gt;mvp.css&lt;/a&gt; for basic styling, and &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/ref/contrib/messages/&quot;&gt;Django generated messages&lt;/a&gt; to the template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- ecommerce/templates/base.html --&amp;gt;

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;One-Product E-commerce Site&amp;lt;/title&amp;gt;

    &amp;lt;!-- include htmx ⬇️ --&amp;gt;
    &amp;lt;script
      src=&amp;quot;https://unpkg.com/htmx.org@1.9.11&amp;quot;
      integrity=&amp;quot;sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0&amp;quot;
      crossorigin=&amp;quot;anonymous&amp;quot;
    &amp;gt;&amp;lt;/script&amp;gt;

    &amp;lt;!-- include mvp.css ⬇️ --&amp;gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://unpkg.com/mvp.css&amp;quot; /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body hx-headers=&amp;#39;{&amp;quot;X-CSRFToken&amp;quot;: &amp;quot;{{ csrf_token }}&amp;quot;}&amp;#39; hx-boost=&amp;quot;true&amp;quot;&amp;gt;
    &amp;lt;header&amp;gt;
      &amp;lt;h1&amp;gt;One-Product E-commerce Site&amp;lt;/h1&amp;gt;
    &amp;lt;/header&amp;gt;
    &amp;lt;main&amp;gt;
      &amp;lt;section&amp;gt;
        {% if messages %} {% for message in messages %}
        &amp;lt;p&amp;gt;&amp;lt;mark&amp;gt;{{ message }}&amp;lt;/mark&amp;gt;&amp;lt;/p&amp;gt;
        {% endfor %} {% endif %}
      &amp;lt;/section&amp;gt;
      {% block content %} {% endblock %}
    &amp;lt;/main&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;code&gt;home.html&lt;/code&gt; template in the same &lt;strong&gt;templates&lt;/strong&gt; directory, for our home view. It should extend the &lt;code&gt;base.html&lt;/code&gt; and just populate its content section.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- ecommerce/templates/home.html --&amp;gt;

{% extends &amp;quot;base.html&amp;quot; %} {% block content %}
&amp;lt;section&amp;gt;{% include &amp;quot;product.html&amp;quot; %}&amp;lt;/section&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this template, we have included the &lt;code&gt;product.html&lt;/code&gt; template. &lt;code&gt;product.html&lt;/code&gt; will render some details about our product and a placeholder image. Let’s create it in the same &lt;strong&gt;templates&lt;/strong&gt; directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- ecommerce/templates/product.html --&amp;gt;
&amp;lt;form&amp;gt;
  &amp;lt;img src=&amp;quot;https://picsum.photos/id/30/400/250&amp;quot; alt=&amp;quot;mug&amp;quot; /&amp;gt;
  &amp;lt;h3&amp;gt;mug&amp;lt;sup&amp;gt;on sale!&amp;lt;/sup&amp;gt;&amp;lt;/h3&amp;gt;
  &amp;lt;p&amp;gt;mugs are great - you can drink coffee on them!&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;5€&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;button type=&amp;quot;submit&amp;quot; id=&amp;quot;submit-btn&amp;quot;&amp;gt;Buy&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create the Home View&lt;/h3&gt;
&lt;p&gt;In &lt;code&gt;ecommerce/views.py&lt;/code&gt;, we&amp;#39;ll create the view that will render the home template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce/views.py

from django.shortcuts import render

def home(request):
    return render(request, &amp;#39;home.html&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And add it to the &lt;code&gt;urlpatterns&lt;/code&gt; in &lt;code&gt;ecommerce_site/urls.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce_site/urls.py

from django.contrib import admin
from django.urls import path
from ecommerce import views # ⬅️ new

urlpatterns = [
    path(&amp;quot;admin/&amp;quot;, admin.site.urls),
    path(&amp;quot;&amp;quot;, views.home, name=&amp;quot;home&amp;quot;), # ⬅️ new
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can run the server with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you jump over to &lt;a href=&quot;http://127.0.0.1:8000/&quot;&gt;http://127.0.0.1:8000&lt;/a&gt; in your browser, you should see something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-08/initial-product-page.png&quot; alt=&quot;initial product page&quot;/&gt;&lt;/p&gt;
&lt;p&gt;It might feel like overkill to add a dedicated &lt;code&gt;product.html&lt;/code&gt; template instead of just the product details in the &lt;code&gt;home.html&lt;/code&gt; template, but &lt;code&gt;product.html&lt;/code&gt; will be useful for the htmx integration.&lt;/p&gt;
&lt;h2&gt;Add the Form and Use Htmx&lt;/h2&gt;
&lt;p&gt;Great! We now have a view that looks good. However, it doesn’t do much yet. We&amp;#39;ll add a form and set up the logic to process our product purchase. Here’s what we want to do:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Allow the user to select how many products (mugs) they want to buy.&lt;/li&gt;
&lt;li&gt;When the user clicks “Buy”, make a POST request to a different view called &lt;code&gt;purchase&lt;/code&gt; in the backend using &lt;a href=&quot;https://htmx.org/attributes/hx-post/&quot;&gt;&lt;code&gt;hx-post&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;In that view, validate the form and wait for 2 seconds to simulate the Stripe integration (we&amp;#39;ll cover this in the second part of this guide).&lt;/li&gt;
&lt;li&gt;Replace the form content with the &lt;code&gt;purchase&lt;/code&gt; view response.&lt;/li&gt;
&lt;li&gt;While the order is processing, show a loading message and disable the “Buy” button, so that the user doesn’t accidentally make multiple orders.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;#39;s go step by step.&lt;/p&gt;
&lt;h3&gt;1: Add a Quantity Order Form&lt;/h3&gt;
&lt;p&gt;Let’s first create and add a simple order form to our view allowing a user to select the number of mugs they want. In &lt;code&gt;ecommerce/forms.py&lt;/code&gt;, add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce/forms.py

from django import forms

class OrderForm(forms.Form):
        quantity = forms.IntegerField(min_value=1, max_value=10, initial=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;ecommerce/views.py&lt;/code&gt;, we can initialize the form in the home view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce/views.py

from ecommerce.forms import OrderForm # ⬅️ new

def home(request):
    form = OrderForm() # ⬅️ new - initialize the form
    return render(request, &amp;quot;home.html&amp;quot;, {&amp;quot;form&amp;quot;: form}) # ⬅️ new - pass the form to the template
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And render it in the template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- ecommerce/templates/product.html --&amp;gt;

&amp;lt;form method=&amp;quot;post&amp;quot;&amp;gt;
  &amp;lt;!-- Same product details as before, hidden for simplicity --&amp;gt;

  &amp;lt;!-- render the form fields ⬇️ --&amp;gt;
  {{ form }}

  &amp;lt;!-- the same submit button as before ⬇️ --&amp;gt;
  &amp;lt;button type=&amp;quot;submit&amp;quot; id=&amp;quot;submit-btn&amp;quot;&amp;gt;Buy&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2: Make a POST Request To a Different View&lt;/h3&gt;
&lt;p&gt;When the user clicks &amp;quot;Buy&amp;quot;, we want to process the corresponding POST request in a dedicated view to separate the different logic of our application. We will use htmx to make this request. In the same &lt;code&gt;ecommerce/templates/product.html&lt;/code&gt; template, let&amp;#39;s extend the form attributes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- ecommerce/templates/product.html --&amp;gt;

&amp;lt;!-- add the hx-post html attribute ⬇️ --&amp;gt;
&amp;lt;form method=&amp;quot;post&amp;quot; hx-post=&amp;quot;{% url &amp;#39;purchase&amp;#39; %}&amp;quot;&amp;gt;
  &amp;lt;!-- Same product details as before, hidden for simplicity --&amp;gt;

  {{ form }}
  &amp;lt;button type=&amp;quot;submit&amp;quot; id=&amp;quot;submit-btn&amp;quot;&amp;gt;Buy&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this attribute, htmx will make a POST request to the &lt;code&gt;purchase&lt;/code&gt; endpoint and stop the page from reloading completely. Now we just need to add the endpoint.&lt;/p&gt;
&lt;h3&gt;3: Create the Purchase View&lt;/h3&gt;
&lt;p&gt;The purchase view can be relatively simple for now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce/views.py
import time # ⬅️ new

# new purchase POST request view ⬇️
@require_POST
def purchase(request):
    form = OrderForm(request.POST)
    if form.is_valid():
        quantity = form.cleaned_data[&amp;quot;quantity&amp;quot;]
        # TODO - add stripe integration to process the order
        # for now, just wait for 2 seconds to simulate the processing
        time.sleep(2)
    return render(request, &amp;quot;product.html&amp;quot;, {&amp;quot;form&amp;quot;: form})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this view, we validate the form, extract the quantity from the cleaned data, and simulate Stripe order processing. In the end, we return the same template (&lt;code&gt;product.html&lt;/code&gt;). We also need to add the view to the &lt;code&gt;urlpatterns&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ecommerce_site/urls.py

# ... same imports as before

urlpatterns = [
    path(&amp;quot;admin/&amp;quot;, admin.site.urls),
    path(&amp;quot;&amp;quot;, views.home, name=&amp;quot;home&amp;quot;),
    path(&amp;quot;purchase&amp;quot;, views.purchase, name=&amp;quot;purchase&amp;quot;),  # ⬅️ new
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now need to tell htmx what to do with this response.&lt;/p&gt;
&lt;h3&gt;4: Replace the Form Content With the Purchase View Response&lt;/h3&gt;
&lt;p&gt;Htmx has a &lt;a href=&quot;https://htmx.org/attributes/hx-swap/&quot;&gt;&lt;code&gt;hx-swap&lt;/code&gt;&lt;/a&gt; attribute which replaces targeted content on the current page with a request&amp;#39;s response.
In our case, since the &lt;code&gt;purchase&lt;/code&gt; view returns the same template, we want to swap its main element — the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- ecommerce/templates/product.html --&amp;gt;

&amp;lt;!-- add the hx-swap html attribute ⬇️ --&amp;gt;
&amp;lt;form method=&amp;quot;post&amp;quot; hx-post=&amp;quot;{% url &amp;#39;purchase&amp;#39; %}&amp;quot; hx-swap=&amp;quot;outerHTML&amp;quot;&amp;gt;
  &amp;lt;!-- Same product details as before, hidden for simplicity --&amp;gt;

  {{ form }}
  &amp;lt;button type=&amp;quot;submit&amp;quot; id=&amp;quot;submit-btn&amp;quot;&amp;gt;Buy&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The value &lt;code&gt;outerHTML&lt;/code&gt; will replace the entire target element with the response from the &lt;code&gt;purchase&lt;/code&gt; view. By default, the htmx target is the same element making the call — in our case, the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element.&lt;/p&gt;
&lt;h3&gt;5: Tell Htmx What to Do When the Request Is Triggered&lt;/h3&gt;
&lt;p&gt;htmx provides some helper utilities for when a request is processing. In our case, we want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disable the &amp;quot;Buy&amp;quot; button (to avoid a form re-submission) by using the &lt;a href=&quot;https://htmx.org/attributes/hx-disabled-elt/&quot;&gt;&lt;code&gt;hx-disabled-elt&lt;/code&gt;&lt;/a&gt; attribute.&lt;/li&gt;
&lt;li&gt;Show a loading message on the screen by using the &lt;a href=&quot;https://htmx.org/docs/#indicators&quot;&gt;&lt;code&gt;htmx-indicator&lt;/code&gt;&lt;/a&gt; class. This class changes its element&amp;#39;s opacity when the request is triggered.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- ecommerce/templates/product.html --&amp;gt;

&amp;lt;!-- add the hx-disabled-elt html attribute ⬇️ --&amp;gt;
&amp;lt;form
  method=&amp;quot;post&amp;quot;
  hx-post=&amp;quot;{% url &amp;#39;purchase&amp;#39; %}&amp;quot;
  hx-swap=&amp;quot;outerHTML&amp;quot;
  hx-disabled-elt=&amp;quot;#submit-btn&amp;quot;
&amp;gt;
  &amp;lt;!-- Same product details as before, hidden for simplicity --&amp;gt;

  {{ form }}
  &amp;lt;button type=&amp;quot;submit&amp;quot; id=&amp;quot;submit-btn&amp;quot;&amp;gt;Buy&amp;lt;/button&amp;gt;

  &amp;lt;!-- add a loading message with the htmx-indicator class ⬇️ --&amp;gt;
  &amp;lt;p class=&amp;quot;htmx-indicator&amp;quot;&amp;gt;&amp;lt;small&amp;gt;Getting your mug ready...&amp;lt;/small&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the customer clicks the &amp;quot;Buy&amp;quot; button, htmx will disable that same button (identifiable by its &lt;code&gt;submit-btn&lt;/code&gt; id), and show a loading message by changing the opacity of the &lt;code&gt;p.htmx-indicator&lt;/code&gt; element.&lt;/p&gt;
&lt;h2&gt;The Result&lt;/h2&gt;
&lt;p&gt;Jump over to the browser.&lt;/p&gt;
&lt;p&gt;You can see the &lt;a href=&quot;https://github.com/franciscobmacedo/ecommerce-site&quot;&gt;final result in this GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It doesn’t do much else yet. But, if form validation fails, errors will display directly on the form, without the need to refresh the entire page. If the form succeeds, we will process the Stripe order accordingly. We&amp;#39;ll see how to do this in the second part of this guide.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ve now set up the basics for our one-product ecommerce site. We&amp;#39;ve configured our Django project, integrated it with htmx to give our site some interactivity, and set up a basic order form for our product.&lt;/p&gt;
&lt;p&gt;In the second part of this guide, we&amp;#39;ll handle orders with Stripe, save the results in our database, and notify users after a successful purchase.&lt;/p&gt;
&lt;p&gt;Until then, happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Monitor the Performance of Your Python Django App with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/07/31/monitor-the-performance-of-your-python-django-app-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2024/07/31/monitor-the-performance-of-your-python-django-app-with-appsignal.html</id>
    <published>2024-07-31T00:00:00+00:00</published>
    <updated>2024-07-31T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s see how we can use AppSignal to monitor performance in a Django application.</summary>
    <content type="html">&lt;p&gt;When we observe a &lt;strong&gt;slow system&lt;/strong&gt;, our first instinct might be to label it as &lt;strong&gt;failing&lt;/strong&gt;. This presumption is widespread and highlights a fundamental truth: performance is synonymous with an application&amp;#39;s maturity and readiness for production.&lt;/p&gt;
&lt;p&gt;In web applications, where milliseconds can determine the success or failure of a user interaction, the stakes are incredibly high. Performance is not just a technical benchmark, but a cornerstone of user satisfaction and operational efficiency.&lt;/p&gt;
&lt;p&gt;Performance encapsulates a system&amp;#39;s responsiveness under varying workloads, quantified by metrics such as CPU and memory utilization, response times, scalability, and throughput.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how AppSignal can monitor and enhance the performance of Django applications.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get started!&lt;/p&gt;
&lt;h2&gt;Django Performance Monitoring Essentials&lt;/h2&gt;
&lt;p&gt;Achieving optimal performance in Django applications involves a multifaceted approach. This means developing applications that run efficiently and maintain their efficiency as they scale. Key metrics are crucial in this process, providing tangible data to guide our optimization efforts. Let&amp;#39;s explore some of these metrics.&lt;/p&gt;
&lt;h3&gt;Key Metrics for Performance Monitoring&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Response Time:&lt;/strong&gt; This is perhaps the most direct indicator of user experience. It measures the time taken for a user&amp;#39;s request to be processed and the response sent back. In a Django application, factors like database queries, view processing, and middleware operations can influence response times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput:&lt;/strong&gt; Throughput refers to the number of requests your application can handle within a given timeframe.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Rate:&lt;/strong&gt; The frequency of errors (4xx and 5xx HTTP responses) can indicate code, database query, or server configuration issues. By monitoring error rates, you can quickly identify and fix problems that might otherwise degrade user experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database Performance Metrics:&lt;/strong&gt; These include the number of queries per request, query execution time, and the efficiency of database connections.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handling Concurrent Users:&lt;/strong&gt; When multiple users access your Django application simultaneously, it is critical to be able to serve all of them efficiently without delays.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What We Will Build&lt;/h2&gt;
&lt;p&gt;In this article, we&amp;#39;ll construct a Django-based e-commerce store ready for high-traffic events, integrating AppSignal to monitor, optimize, and ensure it scales seamlessly under load. We&amp;#39;ll also demonstrate how to enhance an existing application with AppSignal for improved performance (in this case, the &lt;a href=&quot;https://openedx.org/&quot;&gt;Open edX&lt;/a&gt; learning management system).&lt;/p&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;To follow along, you&amp;#39;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.12.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/support/operating-systems.html&quot;&gt;An AppSignal-supported operating system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://appsignal.com/accounts&quot;&gt;An AppSignal account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Basic Django knowledge&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Prepare the Project&lt;/h3&gt;
&lt;p&gt;Now let&amp;#39;s create a directory for our project and clone it from GitHub. We&amp;#39;ll install all the requirements and run a migration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir django-performance &amp;amp;&amp;amp; cd django-performance
python3.12 -m venv venv
source venv/bin/activate
git clone -b main https://github.com/amirtds/mystore
cd mystore
python3.12 -m pip install -r requirements.txt
python3.12 manage.py migrate
python3.12 manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now visit &lt;a href=&quot;http://127.0.0.1:8000&quot;&gt;127.0.0.1:8000&lt;/a&gt;. You should see something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/main_django_app.png&quot; alt=&quot;main django app&quot;/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This Django application is a simple &lt;em&gt;e-commerce store&lt;/em&gt; that provides product lists, details, and a checkout page for users. After successfully cloning and installing the app, use the Django &lt;code&gt;createsuperuser&lt;/code&gt; management command to create a superuser.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now let&amp;#39;s create a couple of products in our application. First, we&amp;#39;ll shell into our Django application by running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3.12 manage.py shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create 3 categories and 3 products:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;from store.models import Category, Product

# Create categories
electronics = Category(name=&amp;#39;Electronics&amp;#39;, description=&amp;#39;Gadgets and electronic devices.&amp;#39;)
books = Category(name=&amp;#39;Books&amp;#39;, description=&amp;#39;Read the world.&amp;#39;)
clothing = Category(name=&amp;#39;Clothing&amp;#39;, description=&amp;#39;Latest fashion and trends.&amp;#39;)

# Save categories to the database
electronics.save()
books.save()
clothing.save()

# Now let&amp;#39;s create new Products with slugs and image URLs
Product.objects.create(
    category=electronics,
    name=&amp;#39;Smartphone&amp;#39;,
    description=&amp;#39;Latest model with high-end specs.&amp;#39;,
    price=799.99,
    stock=30,
    available=True,
    slug=&amp;#39;smartphone&amp;#39;,
    image=&amp;#39;products/iphone_14_pro_max.png&amp;#39;
)

Product.objects.create(
    category=books,
    name=&amp;#39;Python Programming&amp;#39;,
    description=&amp;#39;Learn Python programming with this comprehensive guide.&amp;#39;,
    price=39.99,
    stock=50,
    available=True,
    slug=&amp;#39;python-programming&amp;#39;,
    image=&amp;#39;products/python_programming_book.png&amp;#39;
)

Product.objects.create(
    category=clothing,
    name=&amp;#39;Jeans&amp;#39;,
    description=&amp;#39;Comfortable and stylish jeans for everyday wear.&amp;#39;,
    price=49.99,
    stock=20,
    available=True,
    slug=&amp;#39;jeans&amp;#39;,
    image=&amp;#39;products/jeans.png&amp;#39;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now close the shell and run the server. You should see something like the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/created_products.png&quot; alt=&quot;created products&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Install AppSignal&lt;/h3&gt;
&lt;p&gt;We&amp;#39;ll install &lt;a href=&quot;https://pypi.org/project/appsignal/&quot;&gt;AppSignal&lt;/a&gt; and &lt;a href=&quot;https://pypi.org/project/opentelemetry-instrumentation-django/&quot;&gt;opentelemetry-instrumentation-django&lt;/a&gt; in our project.&lt;/p&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;Before installing these packages, &lt;a href=&quot;https://appsignal.com/users/sign_in&quot;&gt;log in to AppSignal&lt;/a&gt; using your credentials (you can &lt;a href=&quot;https://appsignal.com/users/sign_up&quot;&gt;sign up for a free 30-day trial&lt;/a&gt;). After selecting an organization, click on &lt;strong&gt;Add app&lt;/strong&gt; at the top right of the navigation bar. Choose Python as your language and you&amp;#39;ll receive a &lt;strong&gt;push-api-key&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Make sure your virtual environment is activated and run the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3.12 -m pip install appsignal==1.2.1
python3.12 -m appsignal install --push-api-key [YOU-KEY]
python3.12 -m pip install opentelemetry-instrumentation-django==0.45b0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Provide the app name to the CLI prompt. After installation, you should see a new file called &lt;code&gt;__appsignal__.py&lt;/code&gt; in your project.&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s create a new file called &lt;code&gt;.env&lt;/code&gt; in the project root and add &lt;code&gt;APPSIGNAL_PUSH_API_KEY=YOUR-KEY&lt;/code&gt; (remember to change the value to your actual key). Then, let&amp;#39;s change the content of the &lt;code&gt;__appsignal__.py&lt;/code&gt; file to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# __appsignal__.py
import os
from appsignal import Appsignal

# Load environment variables from the .env file
from dotenv import load_dotenv
load_dotenv()

# Get APPSIGNAL_PUSH_API_KEY from environment
push_api_key = os.getenv(&amp;#39;APPSIGNAL_PUSH_API_KEY&amp;#39;)

appsignal = Appsignal(
    active=True,
    name=&amp;quot;mystore&amp;quot;,
    push_api_key=os.getenv(&amp;quot;APPSIGNAL_PUSH_API_KEY&amp;quot;),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, update the &lt;code&gt;manage.py&lt;/code&gt; file to read like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# manage.py
#!/usr/bin/env python
&amp;quot;&amp;quot;&amp;quot;Django&amp;#39;s command-line utility for administrative tasks.&amp;quot;&amp;quot;&amp;quot;
import os
import sys

# import appsignal
from __appsignal__ import appsignal # new line


def main():
    &amp;quot;&amp;quot;&amp;quot;Run administrative tasks.&amp;quot;&amp;quot;&amp;quot;
    os.environ.setdefault(&amp;quot;DJANGO_SETTINGS_MODULE&amp;quot;, &amp;quot;mystore.settings&amp;quot;)

    # Start Appsignal
    appsignal.start() # new line

    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            &amp;quot;Couldn&amp;#39;t import Django. Are you sure it&amp;#39;s installed and &amp;quot;
            &amp;quot;available on your PYTHONPATH environment variable? Did you &amp;quot;
            &amp;quot;forget to activate a virtual environment?&amp;quot;
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&amp;#39;ve imported AppSignal and started it using the configuration from &lt;code&gt;__appsignal.py&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Please note that the changes we made to &lt;code&gt;manage.py&lt;/code&gt; are for a development environment. In production, we should change &lt;code&gt;wsgi.py&lt;/code&gt; or &lt;code&gt;asgi.py&lt;/code&gt;. For more information, visit &lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/django.html&quot;&gt;AppSignal&amp;#39;s Django documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Project Scenario: Optimizing for a New Year Sale and Monitoring Concurrent Users&lt;/h2&gt;
&lt;p&gt;As we approach the New Year sales on our Django-based e-commerce platform, we recall last year&amp;#39;s challenges: &lt;em&gt;increased traffic&lt;/em&gt; led to &lt;em&gt;slow load times&lt;/em&gt; and even some downtime. This year, we aim to avoid these issues by thoroughly testing and optimizing our site beforehand. We&amp;#39;ll use Locust to simulate user traffic and AppSignal to monitor our application&amp;#39;s performance.&lt;/p&gt;
&lt;h3&gt;Creating a Locust Test for Simulated Traffic&lt;/h3&gt;
&lt;p&gt;First, we&amp;#39;ll create a &lt;code&gt;locustfile.py&lt;/code&gt; file that simulates simultaneous users navigating through critical parts of our site: the homepage, a product detail page, and the checkout page. This simulation helps us understand how our site performs under pressure.&lt;/p&gt;
&lt;p&gt;Create the &lt;code&gt;locustfile.py&lt;/code&gt; in the project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# locustfile.py
from locust import HttpUser, between, task

class WebsiteUser(HttpUser):
    wait_time = between(1, 3)  # Users wait 1-3 seconds between tasks

    @task
    def index_page(self):
        self.client.get(&amp;quot;/store/&amp;quot;)

    @task(3)
    def view_product_detail(self):
        self.client.get(&amp;quot;/store/product/smartphone/&amp;quot;)

    @task(2)
    def view_checkout_page(self):
        self.client.get(&amp;quot;/store/checkout/smartphone/&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;locustfile.py&lt;/code&gt;, users primarily visit the product detail page, followed by the checkout page, and occasionally return to the homepage. This pattern aims to mimic realistic user behavior during a sale.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Before running Locust, ensure you have a product with the &lt;em&gt;smartphone&lt;/em&gt; slug in the Django app. If you don&amp;#39;t, go to &lt;a href=&quot;http://127.0.0.1:8000/admin/store/product/&quot;&gt;/admin/store/product/&lt;/a&gt; and create one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Defining Acceptable Response Times&lt;/h3&gt;
&lt;p&gt;Before we start, let&amp;#39;s define what we consider an acceptable response time. For a smooth user experience, we aim for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Homepage and product detail pages: &lt;strong&gt;under 1 second&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Checkout page: &lt;strong&gt;under 1.5 seconds&lt;/strong&gt; (due to typically higher complexity).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These targets ensure users experience minimal delay, keeping their engagement high.&lt;/p&gt;
&lt;h3&gt;Conducting the Test and Monitoring Results&lt;/h3&gt;
&lt;p&gt;With our Locust test ready, we run it to simulate the 500 users and observe the results in real time. Here&amp;#39;s how:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start the Locust test by running &lt;code&gt;locust -f locustfile.py&lt;/code&gt; in your terminal, then open &lt;a href=&quot;http://localhost:8089&quot;&gt;http://localhost:8089&lt;/a&gt; to set up and start the simulation. Set the &lt;strong&gt;Number of Users&lt;/strong&gt; to 500 and set the host to &lt;a href=&quot;http://127.0.0.1:8000&quot;&gt;http://127.0.0.1:8000&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Monitor performance in both Locust&amp;#39;s web interface and AppSignal. Locust shows us request rates and response times, while AppSignal provides deeper insights into our Django app&amp;#39;s behavior under load.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After running Locust, you can find information about the load test in its dashboard:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/locust_dashboard.png&quot; alt=&quot;locust dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Now, go to your application page in AppSignal. Under the &lt;strong&gt;Performance&lt;/strong&gt; section, click on &lt;em&gt;Actions&lt;/em&gt; and you should see something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/appsignal-performance-action.png&quot; alt=&quot;appsignal performance action&quot;/&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mean:&lt;/strong&gt; This is the average response time for all the requests made to a particular endpoint. It provides a general idea of how long it takes for the server to respond. In our context, any mean response time greater than 1 second could be considered a red flag, indicating that our application&amp;#39;s performance might not meet user expectations for speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90th Percentile:&lt;/strong&gt; This is the response time at the 90th percentile. For example, for &lt;code&gt;GET store/&lt;/code&gt;, we have 7 ms, which means 90% of requests are completed in 7 ms or less.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput:&lt;/strong&gt; The number of requests handled per second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now let&amp;#39;s click on the &lt;em&gt;Graphs&lt;/em&gt; under &lt;em&gt;Performance&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/appsignal-performance-graph.png&quot; alt=&quot;locust dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;We need to prepare our site for the New Year sales, as response times might exceed our targets. Here&amp;#39;s a simplified plan:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Database Queries:&lt;/strong&gt; Slow queries often cause performance issues.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static Assets:&lt;/strong&gt; Ensure static assets are properly cached. Use a CDN for better delivery speeds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application Resources:&lt;/strong&gt; Sometimes, the solution is as straightforward as adding more RAM and CPUs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Database Operations&lt;/h2&gt;
&lt;h3&gt;Understanding Database Performance Impact&lt;/h3&gt;
&lt;p&gt;When it comes to web applications, one of the most common sources of slow performance is database queries. Every time a user performs an action that requires data retrieval or manipulation, a query is made to the database.&lt;/p&gt;
&lt;p&gt;If these queries are not well-optimized, they can take a considerable amount of time to execute, leading to a sluggish user experience. That&amp;#39;s why it&amp;#39;s crucial to monitor and optimize our queries, ensuring they&amp;#39;re efficient and don&amp;#39;t become the bottleneck in our application&amp;#39;s performance.&lt;/p&gt;
&lt;h3&gt;Instrumentation and Spans&lt;/h3&gt;
&lt;p&gt;Before diving into the implementation, let&amp;#39;s clarify two key concepts in performance monitoring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instrumentation&lt;/li&gt;
&lt;li&gt;Spans&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Instrumentation&lt;/strong&gt; is the process of augmenting code to measure its performance and behavior during execution. Think of it like fitting your car with a dashboard that tells you not just the speed, but also the engine&amp;#39;s performance, fuel efficiency, and other diagnostics while you drive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spans&lt;/strong&gt;, on the other hand, are the specific segments of time measured by instrumentation. In our car analogy, a span would be the time taken for a specific part of your journey, like from your home to the highway. In the context of web applications, a span could represent the time taken to execute a database query, process a request, or complete any other discrete operation.&lt;/p&gt;
&lt;p&gt;Instrumentation helps us create a series of spans that together form a detailed timeline of how a request is handled. This timeline is invaluable for pinpointing where delays occur and understanding the overall flow of a request through our system.&lt;/p&gt;
&lt;h3&gt;Implementing Instrumentation in Our Code&lt;/h3&gt;
&lt;p&gt;With our &lt;a href=&quot;https://github.com/amirtds/mystore/blob/main/store/views.py#L37&quot;&gt;PurchaseProductView&lt;/a&gt;, we&amp;#39;re particularly interested in the database interactions that &lt;a href=&quot;https://github.com/amirtds/mystore/blob/main/store/views.py#L42&quot;&gt;create customer records&lt;/a&gt; and &lt;a href=&quot;https://github.com/amirtds/mystore/blob/main/store/views.py#L48&quot;&gt;process purchases&lt;/a&gt;. By adding instrumentation to this view, we&amp;#39;ll be able to measure these interactions and get actionable data on their performance.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s how we integrate AppSignal&amp;#39;s custom instrumentation into our Django view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# store/views.py
# Import OpenTelemetry&amp;#39;s trace module for creating custom spans
from opentelemetry import trace
# Import AppSignal&amp;#39;s set_root_name for customizing the trace name
from appsignal import set_root_name

# Inside the PurchaseProductView
def post(self, request, *args, **kwargs):
    # Initialize the tracer for this view
    tracer = trace.get_tracer(__name__)

    # Start a new span for the view using &amp;#39;with&amp;#39; statement
    with tracer.start_as_current_span(&amp;quot;PurchaseProductView&amp;quot;):
        # Customize the name of the trace to be more descriptive
        set_root_name(&amp;quot;POST /store/purchase/&amp;lt;slug&amp;gt;&amp;quot;)

        # ... existing code to handle the purchase ...

        # Start another span to monitor the database query performance
        with tracer.start_as_current_span(&amp;quot;Database Query - Retrieve or Create Customer&amp;quot;):
            # ... code to retrieve or create a customer ...

            # Yet another span to monitor the purchase record creation
            with tracer.start_as_current_span(&amp;quot;Database Query - Create Purchase Record&amp;quot;):
                # ... code to create a purchase record ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/amirtds/mystore/blob/feat/appsignal-integration/store/views.py#L42&quot;&gt;See the full code of the view after the modification&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In this updated view, custom instrumentation is added to measure the performance of database queries when retrieving or creating a customer and creating a purchase record.&lt;/p&gt;
&lt;p&gt;Now, after purchasing a product in the &lt;em&gt;Slow events&lt;/em&gt; section of the &lt;em&gt;Performance&lt;/em&gt; dashboard, you should see the &lt;code&gt;purchase&lt;/code&gt; event, its performance, and how long it takes to run the query.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/appsignal-performance-events.png&quot; alt=&quot;appsignal performance events&quot;/&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;purchase&lt;/code&gt; is the event we added to our view.&lt;/p&gt;
&lt;h2&gt;Using AppSignal with an Existing Django App&lt;/h2&gt;
&lt;p&gt;In this section, we are going to see how we can integrate AppSignal with &lt;a href=&quot;https://openedx.org/&quot;&gt;Open edX&lt;/a&gt;, an open-source learning management system based on Python and Django.&lt;/p&gt;
&lt;p&gt;Monitoring the performance of learning platforms like Open edX is highly important, since a slow experience directly impacts students&amp;#39; engagement with learning materials and can have a negative impact (for example, a high number of users might decide not to continue with a course).&lt;/p&gt;
&lt;h3&gt;Integrate AppSignal&lt;/h3&gt;
&lt;p&gt;Here, we can follow similar steps as the &lt;strong&gt;Project Setup&lt;/strong&gt; section. However, for Open edX, we will follow &lt;em&gt;Production Setup&lt;/em&gt; and initiate AppSignal in &lt;code&gt;wsgi.py&lt;/code&gt;. &lt;a href=&quot;https://github.com/amirtds/edx-platform/commit/d360768d8db0f1b18a3d2221030a989e4299c323&quot;&gt;Check out this commit&lt;/a&gt; to install and integrate AppSignal with Open edX.&lt;/p&gt;
&lt;h3&gt;Monitor Open edX Performance&lt;/h3&gt;
&lt;p&gt;Now we&amp;#39;ll interact with our platform and see the performance result in the dashboard.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s register a user, log in, enroll them in multiple courses, and interact with the course content.&lt;/p&gt;
&lt;h3&gt;Actions&lt;/h3&gt;
&lt;p&gt;Going to &lt;em&gt;Actions&lt;/em&gt;, let&amp;#39;s order the actions based on their mean time and find slow events:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/openedx-slow-events.png&quot; alt=&quot;openedx slow events&quot;/&gt;&lt;/p&gt;
&lt;p&gt;As we can see, for 3 events (out of the 34 events we tested) the response time is higher than 1 second.&lt;/p&gt;
&lt;h3&gt;Host Metrics&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Host Metrics&lt;/strong&gt; in AppSignal show resource usage:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/openedx-host-metrics.png&quot; alt=&quot;openedx host metrics&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Our system isn&amp;#39;t under heavy load — the load average is 0.03 — but memory usage is high.&lt;/p&gt;
&lt;p&gt;We can also add a trigger to get a notification when resource usage reaches a specific condition. For example, we can set a trigger for when memory usage gets higher than 80% to get notified and prevent outages.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/openedx-memory-trigger.png&quot; alt=&quot;openedx memory trigger&quot;/&gt;&lt;/p&gt;
&lt;p&gt;When we hit the condition, you should get a notification like the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/openedx-trigger-notification.png&quot; alt=&quot;openedx trigger notification&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Celery Tasks Monitoring&lt;/h3&gt;
&lt;p&gt;In Open edX, we use Celery for async and long-running tasks like certificate generation, grading, and bulk email functionality.
Depending on the task and the number of users, some of these tasks can run for a long time and cause performance issues in our platform.&lt;/p&gt;
&lt;p&gt;For example, if thousands of users are enrolled in a course and we need to rescore them, this task can take a while. We might get complaints from users that their grade is not reflected in the dashboard because the task is still running. Having information on Celery tasks, their runtime, and their resource usage gives us important insights and possible points of improvement for our application.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s use AppSignal to trace our Celery tasks in Open edX and see the result in the dashboard. First, make sure &lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/celery.html&quot;&gt;the necessary requirements&lt;/a&gt; are installed. Next, let&amp;#39;s set our tasks to &lt;a href=&quot;https://github.com/amirtds/edx-platform/commit/2a0b8812b778eb0b37a97d4d547ada673ca7652c&quot;&gt;trace Celery performance like in this commit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, let&amp;#39;s run a couple of tasks in the Open edX dashboard to reset attempts and rescore learners&amp;#39; submissions:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/openedx_instructor_tasks.png&quot; alt=&quot;openedx instructor tasks&quot;/&gt;&lt;/p&gt;
&lt;p&gt;We&amp;#39;ll go to the &lt;em&gt;Performance&lt;/em&gt; dashboard in AppSignal -&amp;gt; &lt;em&gt;Slow events&lt;/em&gt; and we will see something like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/openedx-slow-events-celery.png&quot; alt=&quot;openedx slow events celery&quot;/&gt;&lt;/p&gt;
&lt;p&gt;By clicking on Celery, we&amp;#39;ll see all the tasks that have run on Open edX:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/openedx-slow-events-celery-tasks.png&quot; alt=&quot;openedx slow events celery tasks&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This is great information that helps us see if our tasks are running longer than expected, so we can address any possible performance bottlenecks.&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s it!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we saw how AppSignal can give us insights into our Django app&amp;#39;s performance.&lt;/p&gt;
&lt;p&gt;We monitored a simple e-commerce Django application, including metrics like concurrent users, database queries, and response time.&lt;/p&gt;
&lt;p&gt;As a case study, we integrated AppSignal with Open edX, revealing how performance monitoring can be instrumental in enhancing the user experience, particularly for students using the platform.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Monitor the Performance of your Python FastAPI App with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/07/10/monitor-the-performance-of-your-fastapi-for-python-app-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2024/07/10/monitor-the-performance-of-your-fastapi-for-python-app-with-appsignal.html</id>
    <published>2024-07-10T00:00:00+00:00</published>
    <updated>2024-07-10T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s see how we can use AppSignal to monitor performance in a FastAPI application.</summary>
    <content type="html">&lt;p&gt;While building an app with FastAPI can be reasonably straightforward, deploying and operating it might be more challenging.&lt;/p&gt;
&lt;p&gt;The whole user experience can be ruined by unexpected errors, slow responses, or even worse — downtime.&lt;/p&gt;
&lt;p&gt;AppSignal is a great tool of choice for efficiently tracking your FastAPI app&amp;#39;s performance.
It allows you to easily monitor average/95th percentile/90th percentile response times, error rates, throughput, and much more.
Useful charts are available out of the box. Let&amp;#39;s see it in action!&lt;/p&gt;
&lt;h2&gt;What Can You Do with Performance Monitoring?&lt;/h2&gt;
&lt;p&gt;With performance monitoring, we can track app response times, throughput, error rates, CPU consumption, memory usage, etc.
Changes in these metrics can indicate something is not quite right, and we should investigate.&lt;/p&gt;
&lt;p&gt;For example, if response times are monotonically increasing on a specific endpoint, we can investigate what&amp;#39;s causing it. It might be inefficient code, a slow database query, a slow external API call, or something else.&lt;/p&gt;
&lt;p&gt;In such cases, you can intervene before a gateway timeout occurs, for example, and your users start complaining.&lt;/p&gt;
&lt;h2&gt;Setting Up Our FastAPI Python Project and Configuring AppSignal&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s use an app I&amp;#39;ve already prepared.&lt;/p&gt;
&lt;p&gt;First, clone the repository from GitHub:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ git clone git@github.com:jangia/fastapi_performance_with_appsignal.git
$ cd fastapi_performance_with_appsignal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second, create a virtual environment and install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ python3.12 -m venv venv
$ source venv/bin/activate
$ pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;For things to work, you need the following packages installed: &lt;code&gt;opentelemetry-instrumentation-fastapi&lt;/code&gt; and &lt;code&gt;appsignal&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Third, set the AppSignal environment variables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ export APPSIGNAL_PUSH_API_KEY=&amp;lt;your_appsignal_push_api_key&amp;gt;
$ export APPSIGNAL_REVISION=main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;You can read more about configuring AppSignal for FastAPI in &lt;a href=&quot;https://blog.appsignal.com/2024/03/27/track-errors-in-fastapi-for-python-with-appsignal.html&quot;&gt;Track Errors in FastAPI for Python with AppSignal&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Use environment variables to configure AppSignal in the &lt;code&gt;__appsignal__.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name=&amp;quot;fastapi_performance_with_appsignal&amp;quot;,
    push_api_key=os.getenv(&amp;quot;APPSIGNAL_PUSH_API_KEY&amp;quot;),
    revision=os.getenv(&amp;quot;APPSIGNAL_REVISION&amp;quot;),
    enable_host_metrics=True,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The FastAPI app looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
import random
import time

import requests

from appsignal import set_category, set_sql_body, set_body
from fastapi import FastAPI, Depends
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from sqlalchemy.orm import Session

from __appsignal__ import appsignal
from models import SessionLocal, Task

appsignal.start()

tracer = trace.get_tracer(__name__)

app = FastAPI(
    title=&amp;quot;FastAPI with AppSignal&amp;quot;,
)


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


@app.get(&amp;quot;/hello-world&amp;quot;)
def hello_world():
    time.sleep(random.random())
    return {&amp;quot;message&amp;quot;: &amp;quot;Hello World&amp;quot;}

@app.get(&amp;quot;/error&amp;quot;)
def hello_world():
    raise Exception(&amp;quot;Something went wrong. Oops!&amp;quot;)


@app.get(&amp;quot;/slow-external-api&amp;quot;)
def slow_external_api():
    api_url = &amp;quot;http://docs.appsignal.com/&amp;quot;
    with tracer.start_as_current_span(&amp;quot;Call External API&amp;quot;):
        set_category(&amp;quot;external_api.http&amp;quot;)
        set_body(json.dumps({&amp;quot;url&amp;quot;: api_url}))
        response = requests.get(api_url)

    return {&amp;quot;message&amp;quot;: &amp;quot;External API successfully called!&amp;quot;}


@app.get(&amp;quot;/slow-query&amp;quot;)
def slow_query(db: Session = Depends(get_db)):

    with tracer.start_as_current_span(&amp;quot;List tasks&amp;quot;):
        query = db.query(Task)
        tasks = query.all()
        set_category(&amp;quot;tasks.sql&amp;quot;)
        set_sql_body(str(query))
    return {
        &amp;quot;tasks&amp;quot;: [
            {&amp;quot;id&amp;quot;: task.id, &amp;quot;title&amp;quot;: task.title, &amp;quot;status&amp;quot;: task.status}
            for task in tasks
        ],
    }


FastAPIInstrumentor().instrument_app(app)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&amp;#39;ll use a few endpoints to demonstrate how to monitor performance.&lt;/p&gt;
&lt;h2&gt;Monitor Response Times and Throughput in AppSignal&lt;/h2&gt;
&lt;p&gt;With our dependencies installed and environment variables set, we can start the app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(venv)$ uvicorn main:app --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;Once the app is up and running, we can use the &lt;code&gt;call_api.py&lt;/code&gt; script to send requests to the app endpoints in parallel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(venv)$ python call_api.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script will call every endpoint we have 20 times in parallel using &lt;code&gt;asyncio&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio
from aiohttp import ClientSession


async def call_api(url: str):
    async with ClientSession() as session:
        async with session.get(url) as response:
            response = await response.text()
            print(response)


async def main():
    base_url = &amp;quot;http://localhost:8000&amp;quot;
    endpoints = [&amp;quot;slow-query&amp;quot;, &amp;quot;slow-external-api&amp;quot;, &amp;quot;hello-world&amp;quot;, &amp;quot;error&amp;quot;]
    async with asyncio.TaskGroup() as group:
        for endpoint in endpoints:
            url = f&amp;quot;{base_url}/{endpoint}&amp;quot;
            for i in range(20):
                group.create_task(call_api(url))

asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the script completes, go to the AppSignal dashboard and select your application. Once on the application dashboard, choose the &lt;em&gt;Performance tab -&amp;gt; Graphs&lt;/em&gt;. You&amp;#39;ll see two charts — &lt;em&gt;Response Times&lt;/em&gt; and &lt;em&gt;Throughput&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/response-times-and-throughput.png&quot; alt=&quot;Response Times And Throughput&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;Response Times&lt;/em&gt; chart shows the average response time, 95th percentile response time, and 90th percentile response time across all endpoints inside your application.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;Throughput&lt;/em&gt; chart shows the number of processed requests per minute across all endpoints inside your application.&lt;/p&gt;
&lt;p&gt;Looking at these charts is a great way to get a quick overview of how your application is performing, but it&amp;#39;s not enough. Fortunately, AppSignal provides a way to drill into the details — you can see the same charts per endpoint.&lt;/p&gt;
&lt;h3&gt;Digging Deeper&lt;/h3&gt;
&lt;p&gt;To do that, click on &lt;em&gt;Actions&lt;/em&gt; inside &lt;em&gt;Performance&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/endpoints-performance.png&quot; alt=&quot;Endpoints Performance&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll see a list of all endpoints inside your app that were called within the specified time range.&lt;/p&gt;
&lt;p&gt;For each endpoint, you can see the average response time, 95th percentile response time, and 90th percentile response time.
You can go even deeper by clicking on the endpoint name. That will take you to the endpoint details page.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/endpoint-details.png&quot; alt=&quot;Endpoint Details&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll see the same charts as before. This time, they are for the selected endpoint only.&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll see another thing on the endpoint details page — errors and the error rate for the selected endpoint:
&lt;img src=&quot;/images/blog/2024-07/error-rate.png&quot; alt=&quot;Error Rate&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Looking at these charts, you can quickly see whether:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Response times are increasing/decreasing/stable&lt;/li&gt;
&lt;li&gt;Throughput is increasing/decreasing/stable&lt;/li&gt;
&lt;li&gt;Error rate is increasing/decreasing/stable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If any of these metrics show a trend in the wrong direction, you can investigate and fix them before they become a problem for your users.&lt;/p&gt;
&lt;h3&gt;Setting Alerts&lt;/h3&gt;
&lt;p&gt;While this is all great and useful, you must go to the AppSignal dashboard to see how your app is performing.
This way, you might still not react quickly enough when something goes wrong.&lt;/p&gt;
&lt;p&gt;To overcome this, you can set an alert that triggers whenever a request takes longer than the specified threshold.&lt;/p&gt;
&lt;p&gt;Go to the details of a &lt;code&gt;/slow-query&lt;/code&gt; endpoint and click on &lt;em&gt;View Incident&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/view-incident.png&quot; alt=&quot;View Incident&quot;/&gt;&lt;/p&gt;
&lt;p&gt;After that, set alerts on the right side, with &lt;em&gt;5&lt;/em&gt; seconds as the threshold, and select &lt;em&gt;Every occurrence&lt;/em&gt; as Alerting:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/set-alert.png&quot; alt=&quot;Set Alert&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Run the script that calls the endpoints one more time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(venv)$ python call_api.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should receive an email from AppSignal warning you about the request/s that take/took too long to process. This means you don&amp;#39;t need to constantly check the AppSignal dashboard to see how your app is performing. You&amp;#39;ll be notified straight away if something strange happens.&lt;/p&gt;
&lt;h2&gt;Monitor Database Queries&lt;/h2&gt;
&lt;p&gt;Monitoring response times and throughput is very useful, but it only tells us a little about what&amp;#39;s causing a problem.&lt;/p&gt;
&lt;p&gt;As mentioned, multiple things can cause slow response times.&lt;/p&gt;
&lt;p&gt;One is slow database queries: we&amp;#39;ll also want to monitor them.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s take a look at the &lt;code&gt;/slow-query&lt;/code&gt; endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ... other code
tracer = trace.get_tracer(__name__)
# ... other code


@app.get(&amp;quot;/slow-query&amp;quot;)
def slow_query(db: Session = Depends(get_db)):

    with tracer.start_as_current_span(&amp;quot;List tasks&amp;quot;):
        query = db.query(Task)
        tasks = query.all()
        set_category(&amp;quot;tasks.sql&amp;quot;)
        set_sql_body(str(query))
    return {
        &amp;quot;tasks&amp;quot;: [
            {&amp;quot;id&amp;quot;: task.id, &amp;quot;title&amp;quot;: task.title, &amp;quot;status&amp;quot;: task.status}
            for task in tasks
        ],
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we&amp;#39;re using SQLAlchemy to query the database for all tasks. To track queries inside FastAPI, we need to utilize custom instrumentation.&lt;/p&gt;
&lt;p&gt;We can do that by executing the query inside the &lt;code&gt;with tracer.start_as_current_span(&amp;quot;List tasks&amp;quot;):&lt;/code&gt; block.
We&amp;#39;ll create a &amp;quot;List tasks&amp;quot; span. You&amp;#39;ll see it inside the AppSignal dashboard.&lt;/p&gt;
&lt;p&gt;We execute the query inside the span. We need to set a category for AppSignal to recognize that this is a database query measurement using &lt;code&gt;set_category(&amp;quot;tasks.sql&amp;quot;)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The category name must end with one of the following suffixes to be recognized as a database query:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;*.active_record&lt;/li&gt;
&lt;li&gt;*.ecto&lt;/li&gt;
&lt;li&gt;*.elasticsearch&lt;/li&gt;
&lt;li&gt;*.knex&lt;/li&gt;
&lt;li&gt;*.mongodb&lt;/li&gt;
&lt;li&gt;*.mysql&lt;/li&gt;
&lt;li&gt;*.postgres&lt;/li&gt;
&lt;li&gt;*.psycopg2&lt;/li&gt;
&lt;li&gt;*.redis&lt;/li&gt;
&lt;li&gt;*.sequel&lt;/li&gt;
&lt;li&gt;*.sql&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also add a query string to the span body — &lt;code&gt;set_sql_body(str(query))&lt;/code&gt;. This way, we can see the executed query inside the AppSignal dashboard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;&lt;code&gt;str(query)&lt;/code&gt; returns a query with placeholders. If you want to see a query with values, you can use &lt;code&gt;str(query.statement.compile(compile_kwargs={&amp;quot;literal_binds&amp;quot;: True}))&lt;/code&gt;. But be careful not to send any sensitive data this way.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;A span is a single step in the execution flow.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Since you&amp;#39;ve already executed the script that calls the endpoints, you should see the &lt;code&gt;/slow-query&lt;/code&gt; endpoint query inside the AppSignal dashboard.&lt;/p&gt;
&lt;p&gt;Go to &lt;em&gt;Slow queries&lt;/em&gt; under &lt;em&gt;Performance&lt;/em&gt;:
&lt;img src=&quot;/images/blog/2024-07/slow-queries.png&quot; alt=&quot;Slow Queries&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You can click on the query name &lt;code&gt;tasks.sql&lt;/code&gt; to see the query details. There you&amp;#39;ll find:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The query itself (as we sent it, with &lt;code&gt;set_sql_body&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Response times chart&lt;/li&gt;
&lt;li&gt;Throughput chart&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Looking at the charts, you can see the query&amp;#39;s performance trend. If the response times are monotonically increasing, you can investigate why and fix the issue.&lt;/p&gt;
&lt;p&gt;Hint: some possible causes for slow queries are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queries are not using indexes (e.g., there&amp;#39;s a missing index or filtering is only set on non-indexed columns).&lt;/li&gt;
&lt;li&gt;Queries are loading all the data from a database (e.g., there&amp;#39;s missing pagination or all the data for rows is being loaded despite this being unnecessary).&lt;/li&gt;
&lt;li&gt;N + 1 queries (e.g., querying for all users and then querying each user&amp;#39;s tasks to satisfy a single request).&lt;/li&gt;
&lt;li&gt;Inefficient query planning (e.g., &lt;em&gt;max&lt;/em&gt; set on an empty result can take an abnormally long time in PostgreSQL).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Monitor Slow External API Calls&lt;/h2&gt;
&lt;p&gt;External API calls can also cause slow response times. Since external APIs are outside your control, you just have to hope for the best.&lt;/p&gt;
&lt;p&gt;In reality, external APIs often cause problems due to being unresponsive or even down entirely.
That&amp;#39;s why you should monitor those calls as well.&lt;/p&gt;
&lt;p&gt;As with database queries, AppSignal has got you covered.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s take a look at the &lt;code&gt;/slow-external-api&lt;/code&gt; endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.get(&amp;quot;/slow-external-api&amp;quot;)
def slow_external_api():
    api_url = &amp;quot;http://docs.appsignal.com/&amp;quot;
    with tracer.start_as_current_span(&amp;quot;Call External API&amp;quot;):
        set_category(&amp;quot;external_api.http&amp;quot;)
        set_body(json.dumps({&amp;quot;url&amp;quot;: api_url}))
        requests.get(api_url)

    return {&amp;quot;message&amp;quot;: &amp;quot;External API successfully called!&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Like with database queries, we need to use custom instrumentation to track external API calls.
We can do that by executing the query inside the &lt;code&gt;with tracer.start_as_current_span(&amp;quot;Call External API&amp;quot;)&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;We set the span name to &amp;quot;Call External API&amp;quot;. You&amp;#39;ll see it inside the AppSignal dashboard.&lt;/p&gt;
&lt;p&gt;Inside the span, we execute the external API call. Set the correct category for AppSignal to recognize that this is an external API call measurement — &lt;code&gt;set_category(&amp;quot;external_api.http&amp;quot;)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The category name must end with one of the following suffixes to be recognized as an external API call:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;*.faraday&lt;/li&gt;
&lt;li&gt;*.grpc&lt;/li&gt;
&lt;li&gt;*.http&lt;/li&gt;
&lt;li&gt;*.http_rb&lt;/li&gt;
&lt;li&gt;*.net_http&lt;/li&gt;
&lt;li&gt;*.excon&lt;/li&gt;
&lt;li&gt;*.request&lt;/li&gt;
&lt;li&gt;*.requests&lt;/li&gt;
&lt;li&gt;*.service&lt;/li&gt;
&lt;li&gt;*.finch&lt;/li&gt;
&lt;li&gt;*.tesla&lt;/li&gt;
&lt;li&gt;*.fetch&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also add a called URL to the span body — &lt;code&gt;set_body(json.dumps({&amp;quot;url&amp;quot;: api_url}))&lt;/code&gt;. This lets us see which URL was called inside the AppSignal dashboard.
You can add more details to the span&amp;#39;s body by extending the dictionary you&amp;#39;re sending to &lt;code&gt;set_body&lt;/code&gt;. (For example, you can also add a request or response body).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;Ensure you don&amp;#39;t send sensitive data inside the span&amp;#39;s body.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Since you&amp;#39;ve already executed the script that calls the endpoints, you should see the &lt;code&gt;/slow-external-api&lt;/code&gt; endpoint query inside the AppSignal dashboard.&lt;/p&gt;
&lt;p&gt;Go to &lt;em&gt;Slow queries&lt;/em&gt; under &lt;em&gt;Performance&lt;/em&gt;:
&lt;img src=&quot;/images/blog/2024-07/slow-external-api-calls.png&quot; alt=&quot;Slow External API Calls&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You can click on the API call name &lt;code&gt;external_api.http&lt;/code&gt; to see the details. There you&amp;#39;ll find:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The URL that was called (as we sent it with &lt;code&gt;set_body&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Response times chart&lt;/li&gt;
&lt;li&gt;Throughput chart&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Looking at the charts, you can see how the API call performs over time. You can decide whether to increase the timeout for the API call or change API usage (e.g., use a smaller page size or utilize a different API).&lt;/p&gt;
&lt;h2&gt;Monitor Host Metrics in AppSignal for FastAPI&lt;/h2&gt;
&lt;p&gt;Last but not least, you can monitor host metrics. AppSignal can track CPU, memory, disk, and network usage. You can enable that by setting &lt;code&gt;enable_host_metrics=True&lt;/code&gt; inside the &lt;code&gt;__appsignal__.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name=&amp;quot;fastapi_performance_with_appsignal&amp;quot;,
    push_api_key=os.getenv(&amp;quot;APPSIGNAL_PUSH_API_KEY&amp;quot;),
    revision=&amp;quot;main&amp;quot;,
    enable_host_metrics=True,  # THIS
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So stop &lt;em&gt;uvicorn&lt;/em&gt;, and let&amp;#39;s run our API with &lt;em&gt;docker-compose&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ docker-compose up -d --build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once again, run the script that calls the endpoints:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(venv)$ python call_api.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;macOS/OSX is not supported. That&amp;#39;s why we&amp;#39;re using Docker. See &lt;a href=&quot;https://docs.appsignal.com/metrics/host-metrics.html&quot;&gt;AppSignal&amp;#39;s official docs&lt;/a&gt; for more info.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once the script completes, go to the AppSignal dashboard under &lt;em&gt;Performance&lt;/em&gt; -&amp;gt; &lt;em&gt;Host metrics&lt;/em&gt; — you&amp;#39;ll see the list of hosts that are running your app:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-07/host-metrics.png&quot; alt=&quot;Host Metrics&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Click on the hostname to see the details. There, you&amp;#39;ll find these charts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load Average&lt;/li&gt;
&lt;li&gt;CPU Usage&lt;/li&gt;
&lt;li&gt;Memory Usage&lt;/li&gt;
&lt;li&gt;Swap Usage&lt;/li&gt;
&lt;li&gt;Disk I/O Read&lt;/li&gt;
&lt;li&gt;Disk I/O Write&lt;/li&gt;
&lt;li&gt;Disk Usage&lt;/li&gt;
&lt;li&gt;Network Traffic Received&lt;/li&gt;
&lt;li&gt;Network Traffic Transmitted&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These metrics can help you determine whether your hosts have enough resources to handle the load.
You can also see whether the load is roughly equally spread across the hosts.&lt;/p&gt;
&lt;p&gt;If you see anything that concerns you, you can investigate further and take action before it becomes a problem for your users.&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s it!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this post, we&amp;#39;ve seen how to monitor the performance of FastAPI applications using AppSignal.&lt;/p&gt;
&lt;p&gt;Monitoring performance means that we can intervene before things go south and our users start complaining.&lt;/p&gt;
&lt;p&gt;To be fully in control of our app, we should combine performance monitoring with error tracking.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Deploy a Python FastAPI Application to Render</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/06/26/deploy-a-python-fastapi-application-to-render.html"/>
    <id>https://blog.appsignal.com/2024/06/26/deploy-a-python-fastapi-application-to-render.html</id>
    <published>2024-06-26T00:00:00+00:00</published>
    <updated>2024-06-26T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s create a FastAPI app and deploy it to Render.</summary>
    <content type="html">&lt;p&gt;In the world of Python frameworks, FastAPI is the new kid on the block and a great choice for building APIs. Equally, Render is a good option for developers who want to quickly test their applications in a production environment for free.&lt;/p&gt;
&lt;p&gt;In this post, we&amp;#39;ll run through how to deploy a FastAPI app to Render. First, though, let&amp;#39;s explore why FastAPI and Render are often chosen by developers.&lt;/p&gt;
&lt;h2&gt;Why FastAPI?&lt;/h2&gt;
&lt;p&gt;FastAPI is a high-performance microframework used primarily for building APIs (the clue is in the name). As such, FastAPI offers several advantages over older, better-known frameworks such as Django and Flask.&lt;/p&gt;
&lt;p&gt;The first and most obvious advantage of FastAPI is that it was built with scalability and performance in mind.&lt;/p&gt;
&lt;p&gt;For example, FastAPI is based on an asynchronous ASGI server rather than the older WSGI used by Django and other frameworks. ASGI servers can handle multiple requests simultaneously (concurrently). This is often seen as a better option for apps that need to handle high levels of user traffic.&lt;/p&gt;
&lt;p&gt;FastAPI also makes it easier for developers to write asynchronous code by simply using the &lt;code&gt;async&lt;/code&gt; keyword when defining asynchronous functions.&lt;/p&gt;
&lt;p&gt;But FastAPI&amp;#39;s shiny &amp;quot;newness&amp;quot; is also its main drawback. Because it was only introduced in 2018, FastAPI has a much smaller community and fewer learning resources (such as coding tutorials) compared to the more established frameworks. This is something to bear in mind when choosing FastAPI for your next project.&lt;/p&gt;
&lt;h2&gt;Why Render?&lt;/h2&gt;
&lt;p&gt;Render is a great option for developers who want to quickly test their applications in a production environment for free.&lt;/p&gt;
&lt;p&gt;With Render, you don&amp;#39;t even have to enter your credit card details to gain access to the free tier. So unlike other cloud services, there is no way to rack up charges by mistake.&lt;/p&gt;
&lt;p&gt;As we will see in the section below, Render also provides an excellent overall developer experience with a clean, very easy-to-use UI and smooth integration with Git and GitHub. An added bonus is that any app you host on Render gets a free TLS certificate right out of the box.&lt;/p&gt;
&lt;p&gt;The downside with Render&amp;#39;s free tier is that it can take a long time for deployments to complete. Once this starts to become an issue, you might want to consider upgrading to one of the paid plans.&lt;/p&gt;
&lt;h2&gt;Create a FastAPI Demo Application&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;You can &lt;a href=&quot;https://github.com/daneasterman/fastapi-render-appsignal&quot;&gt;view, download, or clone the full code&lt;/a&gt; used in this article.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The first thing we need to do is create an empty directory where our project will live. Let&amp;#39;s call it &lt;code&gt;fastapi-render-appsignal&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Run the following commands in your terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mkdir fastapi-render-appsignal
cd flask-heroku-appsignal
touch main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The entry point for the project will be &lt;code&gt;main.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, we need to make our project a git repository by running the git initialize command in the root of the directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, create a &lt;code&gt;requirements.txt&lt;/code&gt; file in the project&amp;#39;s root directory. Add these two lines to the file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;fastapi
uvicorn[standard]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the installation goes smoothly, you should see the following output in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Installing collected packages: websockets, uvloop, typing-extensions, sniffio, pyyaml, python-dotenv, idna, httptools, h11, exceptiongroup, click, annotated-types, uvicorn, pydantic-core, anyio, watchfiles, starlette, pydantic, fastapi
Successfully installed annotated-types-0.6.0 anyio-4.2.0 click-8.1.7 exceptiongroup-1.2.0 fastapi-0.109.0 h11-0.14.0 httptools-0.6.1 idna-3.6 pydantic-2.5.3 pydantic-core-2.14.6 python-dotenv-1.0.1 pyyaml-6.0.1 sniffio-1.3.0 starlette-0.35.1 typing-extensions-4.9.0 uvicorn-0.27.0 uvloop-0.19.0 watchfiles-0.21.0 websockets-12.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, in &lt;code&gt;main.py&lt;/code&gt;, copy and paste the following barebones boilerplate code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import FastAPI

app = FastAPI()

@app.get(&amp;quot;/&amp;quot;)
async def root():
    return {&amp;quot;message&amp;quot;: &amp;quot;Hello World&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#39;s go line-by-line and explain what is being done in this short code snippet.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;After the import statements, we create an instance of the &lt;code&gt;FastAPI&lt;/code&gt; class and assign it to the variable &lt;code&gt;app&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@app.get(&amp;quot;/&amp;quot;)&lt;/code&gt; is a Python decorator that tells FastAPI to create an API endpoint via the HTTP GET method at the root URL &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;async def root()&lt;/code&gt; defines an asynchronous function named root. We are using the async keyword here because FastAPI is built to work with asynchronous I/O operations. This means it can handle concurrent requests, which is better for performance.&lt;/li&gt;
&lt;li&gt;In the body of the function, we return a Python dictionary which FastAPI helpfully converts to JSON for us. So, when the user goes to the root &lt;code&gt;/&lt;/code&gt; endpoint, our API will automatically respond with a JSON object.&lt;/li&gt;
&lt;/ol&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;We can now run our project using the server package &lt;code&gt;Uvicorn&lt;/code&gt; (that we installed earlier) with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uvicorn main:app --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we open our browser at &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;, we will just see the following plain JSON response:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{ &amp;quot;message&amp;quot;: &amp;quot;Hello World&amp;quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But if we add &lt;code&gt;/docs&lt;/code&gt; to the end of the URL, we can take advantage of FastAPI&amp;#39;s in-built interactive API documentation capabilities.&lt;/p&gt;
&lt;p&gt;The documentation UI in the screenshot below is provided by Swagger UI:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/swagger.png&quot; alt=&quot;FastAPI Swagger UI API documentation screen&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You can also try another documentation UI style that&amp;#39;s automatically included with FastAPI.&lt;/p&gt;
&lt;p&gt;Enter the URL &lt;code&gt;http://127.0.0.1:8000/redoc&lt;/code&gt; in your browser.&lt;/p&gt;
&lt;p&gt;This one is provided by ReDoc:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/redoc.png&quot; alt=&quot;FastAPI ReDoc API documentation screen&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Deploy on Render&lt;/h2&gt;
&lt;p&gt;This part assumes you have already created a repository for this project on GitHub. If you need instructions on how to do this, check out &lt;a href=&quot;https://docs.github.com/en/repositories/creating-and-managing-repositories/quickstart-for-repositories&quot;&gt;GitHub’s official docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One of Render’s advantages is that it provides an easy and intuitive way to connect to your GitHub repository.&lt;/p&gt;
&lt;p&gt;First, go to &lt;a href=&quot;https://render.com&quot;&gt;render.com&lt;/a&gt; and create a new account with Render (you can use your GitHub credentials to speed things up).&lt;/p&gt;
&lt;p&gt;Then click on the &lt;strong&gt;New&lt;/strong&gt; button and select the option to create a new web service.&lt;/p&gt;
&lt;p&gt;After linking your GitHub account, you should see the screen below, which provides a list of GitHub repos to connect to Render. Let&amp;#39;s select our &lt;strong&gt;fastapi-render-appsignal&lt;/strong&gt; example project.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/github-repos.png&quot; alt=&quot;List of GitHub repos in Render&quot;/&gt;&lt;/p&gt;
&lt;p&gt;This will then take us to the main configuration screen. Under region, simply select the region closest to your location (since I’m based in the UK, I will choose Frankfurt):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/config-region.png&quot; alt=&quot;Render config screen with region option&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Further down the screen, we have a few more important options.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;build command&lt;/strong&gt; will use the &lt;code&gt;requirements.txt&lt;/code&gt; file we created earlier in the project to install all the packages from our project on Render’s remote server.&lt;/p&gt;
&lt;p&gt;Under &lt;strong&gt;start command&lt;/strong&gt;, we will use Uvicorn again in our production environment and tell Render to start the application using &lt;code&gt;main.py&lt;/code&gt; in the root of our project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;You must explicitly tell Render which host and port to use (unlike when using other servers like gunicorn). Unfortunately, this is not mentioned in any of the official Render documentation but I found the solution &lt;a href=&quot;https://community.render.com/t/fastapi-python-web-service-deploying-for-hours/6662&quot;&gt;in the Render community forum&lt;/a&gt; and in the post &lt;a href=&quot;https://blog.akashrchandran.in/deploying-fastapi-application-to-render&quot;&gt;&amp;#39;Deploying FastAPI application to Render&amp;#39;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/render-config.png&quot; alt=&quot;Render config screen with start and build command settings&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Next, choose the free option under the list of instance types:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/render-instance-types.png&quot; alt=&quot;Render instance types with pricing options&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Lastly, under environment variables, specify the Python version as &lt;code&gt;3.10.7&lt;/code&gt;. Ensure this matches the same version you are using in your local project.&lt;/p&gt;
&lt;p&gt;With our web service configuration done, now we simply click the &lt;strong&gt;Create Web Service&lt;/strong&gt; button and watch the project get deployed:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/deployment-log.png&quot; alt=&quot;Render screen showing web service deployment process log&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Remember, this will take a bit of time since we are using the free plan.&lt;/p&gt;
&lt;p&gt;If everything goes smoothly at the end of the deployment process, we should see a final message in Render&amp;#39;s logs saying: &lt;strong&gt;&amp;quot;Your service is live&amp;quot;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/service-live-logs.png&quot; alt=&quot;Render screen showing service is live in logs&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Now we can click on the public link Render has generated for us and see our &lt;strong&gt;Hello World&lt;/strong&gt; FastAPI example, including the automatic documentation links at &lt;code&gt;/docs&lt;/code&gt; and &lt;code&gt;/redoc&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/deployed-render-url.png&quot; alt=&quot;Screen showing service deployed at Render URL&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Installing the AppSignal Dashboard for FastAPI and Monitoring Deploys&lt;/h2&gt;
&lt;p&gt;It&amp;#39;s great that we now have a basic FastAPI app running and deployed to Render, but what if something goes wrong? Even with the best manual and automated testing, the reality is that errors and bugs happen in software development. The key thing is to get alerted as soon as errors happen and to quickly identify where the problem is in the code.&lt;/p&gt;
&lt;p&gt;In this section of the tutorial, we are going to specifically look at how to enable AppSignal to monitor your deployments. This part of the setup is crucial for you to see which errors are associated with a specific deployment. This will allow you to diagnose and fix problems in your app quickly and efficiently.&lt;/p&gt;
&lt;p&gt;First, &lt;a href=&quot;https://appsignal.com/users/sign_up&quot;&gt;sign up for an AppSignal account&lt;/a&gt; (you can do a 30-day free trial, no credit card details required).
Next, add &lt;code&gt;appsignal&lt;/code&gt; and &lt;code&gt;opentelemetry-instrumentation-fastapi&lt;/code&gt; to your &lt;code&gt;requirements.txt&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# requirements.txt
appsignal
opentelemetry-instrumentation-fastapi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to create an &lt;code&gt;appsignal.py&lt;/code&gt; configuration file and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os
import subprocess
from appsignal import Appsignal
from dotenv import load_dotenv
load_dotenv()

revision = None
try:
    revision = subprocess.check_output(
        &amp;quot;git log --pretty=format:&amp;#39;%h&amp;#39; -n 1&amp;quot;, shell=True
    ).strip()
except subprocess.CalledProcessError:
  pass


appsignal = Appsignal(
    active=True,
    name=&amp;quot;fastapi-render-appsignal&amp;quot;,
    push_api_key=os.getenv(&amp;quot;APPSIGNAL_API_KEY&amp;quot;),
    revision=revision,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the code above, we initialize the &lt;code&gt;Appsignal&lt;/code&gt; object with a few different parameters, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setting &lt;code&gt;active&lt;/code&gt; to &lt;code&gt;True&lt;/code&gt; to enable monitoring.&lt;/li&gt;
&lt;li&gt;Providing the &lt;code&gt;name&lt;/code&gt; for our application.&lt;/li&gt;
&lt;li&gt;Putting the API key in an environment variable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the key parameter to enable deploy tracking is the &lt;code&gt;revision&lt;/code&gt; parameter. Inside the &lt;code&gt;try-except&lt;/code&gt; block (above the AppSignal initialization code), we fetch the latest commit hash so AppSignal can keep track of our deploys.&lt;/p&gt;
&lt;p&gt;Lastly, we need to update &lt;code&gt;main.py&lt;/code&gt; to finish setting up AppSignal in our FastAPI app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# main.py
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor #new
import appsignal #new

appsignal.start() #new

app = FastAPI()

@app.get(&amp;quot;/&amp;quot;)
async def root():
    return {&amp;quot;message&amp;quot;: &amp;quot;Hello World&amp;quot;}

@app.get(&amp;quot;/items/{item_id}&amp;quot;)
async def get_item(item_id: int):
    return {&amp;quot;item_id&amp;quot;: item_id}

FastAPIInstrumentor().instrument_app(app) #new
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we check out our AppSignal dashboard, the first thing we will see is the &lt;strong&gt;Getting Started&lt;/strong&gt; screen which will provide some suggestions on additional configuration steps to complete:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/getting-started.png&quot; alt=&quot;Getting started screen in AppSignal dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;But here we&amp;#39;ll just focus on ensuring that AppSignal can track our deploys.&lt;/p&gt;
&lt;p&gt;Go to the &lt;strong&gt;Organization&lt;/strong&gt; page in the AppSignal dashboard and then click on &lt;strong&gt;Organization settings&lt;/strong&gt; to activate AppSignal&amp;#39;s integration with GitHub. &lt;a href=&quot;https://docs.appsignal.com/application/integrations/github/link-to-repo.html&quot;&gt;Check out AppSignal&amp;#39;s official docs on linking your repo&lt;/a&gt; for step-by-step instructions.&lt;/p&gt;
&lt;p&gt;With the GitHub integration set up and your repo linked, all you need to do now is trigger a deployment in Render for AppSignal to start monitoring your deploys.&lt;/p&gt;
&lt;p&gt;In our case, we simply need to push to our main branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git push origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything is set up correctly, we should see AppSignal tracking our deploys and any errors associated with each deploy commit hash in the &lt;strong&gt;Deploys&lt;/strong&gt; section of the dashboard:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-06/deploys-fastapi.png&quot; alt=&quot;Deploys screen in AppSignal dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s it!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this guide, we covered a lot of ground! First, we briefly introduced some of the advantages and disadvantages of choosing FastAPI and Render.&lt;/p&gt;
&lt;p&gt;Then, we created a simple FastAPI app, deployed it to Render, and ended by monitoring those deploys with AppSignal.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;https://blog.appsignal.com/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post!&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Track Errors in Your Python Flask Application with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/05/29/track-errors-in-your-python-flask-application-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2024/05/29/track-errors-in-your-python-flask-application-with-appsignal.html</id>
    <published>2024-05-29T00:00:00+00:00</published>
    <updated>2024-05-29T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s track and fix errors in a Flask application using AppSignal.</summary>
    <content type="html">&lt;p&gt;In this article, we&amp;#39;ll look at how to track errors in a Flask application using AppSignal.&lt;/p&gt;
&lt;p&gt;We&amp;#39;ll first bootstrap a Flask project, and install and configure AppSignal. Then, we&amp;#39;ll introduce some faulty code and demonstrate how to track and resolve errors using AppSignal&amp;#39;s Errors dashboard.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get started!&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before diving into the article, ensure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.8+&lt;/a&gt; installed on your local machine&lt;/li&gt;
&lt;li&gt;An &lt;a href=&quot;https://docs.appsignal.com/support/operating-systems.html&quot;&gt;AppSignal-supported operating system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;An &lt;a href=&quot;https://appsignal.com/users/sign_in&quot;&gt;AppSignal account&lt;/a&gt; (you can start a free 30-day trial)&lt;/li&gt;
&lt;li&gt;Fundamental Flask knowledge&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;p&gt;To demonstrate how AppSignal error tracking works, we&amp;#39;ll create a simple TODO app. The app will provide a RESTful API that supports CRUD operations. Initially, it will contain some faulty code, which we&amp;#39;ll address later.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I recommend you first follow along with this exact project since the article is tailored to it. After the article, you&amp;#39;ll, of course, be able to integrate AppSignal into your own Flask projects.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Start by bootstrapping a Flask project:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create and activate a virtual environment&lt;/li&gt;
&lt;li&gt;Use pip to install the latest version of Flask&lt;/li&gt;
&lt;li&gt;Start the development server&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;If you get stuck, refer to the &lt;a href=&quot;https://flask.palletsprojects.com/en/3.0.x/installation/&quot;&gt;Flask Installation guide&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The source code for this project can be found in the &lt;a href=&quot;https://github.com/duplxey/appsignal-flask-error-tracking&quot;&gt;appsignal-flask-error-tracking&lt;/a&gt; GitHub repo.&lt;/p&gt;
&lt;h2&gt;Install AppSignal for Flask&lt;/h2&gt;
&lt;p&gt;To add AppSignal to your Flask project, follow the AppSignal documentation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/installation&quot;&gt;AppSignal Python Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/flask.html&quot;&gt;AppSignal Flask Instrumentation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ensure everything works by starting the development server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ flask run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your app should automatically send a demo error to AppSignal. From now on, all your app errors will be forwarded to AppSignal.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you get an error saying &lt;code&gt;Failed to find Flask application&lt;/code&gt;, you most likely imported Flask before starting the AppSignal client. As mentioned in the &lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/flask.html#setup&quot;&gt;docs&lt;/a&gt;, AppSignal has to be imported and started at the top of &lt;em&gt;app.py&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Flask for Python App Logic&lt;/h2&gt;
&lt;p&gt;Moving along, let&amp;#39;s implement the web app logic.&lt;/p&gt;
&lt;h3&gt;Flask-SQLAlchemy for the Database&lt;/h3&gt;
&lt;p&gt;We&amp;#39;ll use the &lt;a href=&quot;https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/&quot;&gt;Flask-SQLAlchemy&lt;/a&gt; package to manage the database. This package provides &lt;a href=&quot;https://www.sqlalchemy.org/&quot;&gt;SQLAlchemy&lt;/a&gt; support to Flask projects. That includes the Python SQL toolkit and the ORM.&lt;/p&gt;
&lt;p&gt;First, install it via pip:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ pip install Flask-SQLAlchemy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then initialize the database and Flask:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# app.py

db = SQLAlchemy()
app.config[&amp;quot;SQLALCHEMY_TRACK_MODIFICATIONS&amp;quot;] = False
app.config[&amp;quot;SQLALCHEMY_DATABASE_URI&amp;quot;] = &amp;quot;sqlite:///default.db&amp;quot;
db.init_app(app)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don&amp;#39;t forget about the import:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask_sqlalchemy import SQLAlchemy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create the &lt;code&gt;Task&lt;/code&gt; database model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# app.py

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128), nullable=False)
    description = db.Column(db.Text(512), nullable=True)

    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())

    is_done = db.Column(db.Boolean, default=False)

    def as_dict(self):
        return {c.name: getattr(self, c.name) for c in self.__table__.columns}

    def __repr__(self):
        return f&amp;quot;&amp;lt;Task {self.id}&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each &lt;code&gt;Task&lt;/code&gt; will have a &lt;code&gt;name&lt;/code&gt;, an optional &lt;code&gt;description&lt;/code&gt;, an &lt;code&gt;is_done&lt;/code&gt; field, and some administrative data. To serialize the &lt;code&gt;Task&lt;/code&gt;, we&amp;#39;ll use its &lt;code&gt;as_dict()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;Since Flask-SQLAlchemy doesn&amp;#39;t automatically create the database and its structure, we must do it ourselves. To handle that, we&amp;#39;ll create a simple Python script.&lt;/p&gt;
&lt;p&gt;Create an &lt;em&gt;init_db.py&lt;/em&gt; file in the project root with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# init_db.py

from app import Task
from app import db, app

with app.app_context():
    db.create_all()

    if Task.query.count() == 0:
        tasks = [
            Task(
                name=&amp;quot;Deploy App&amp;quot;,
                description=&amp;quot;Deploy the Flask app to the cloud.&amp;quot;,
                is_done=False,
            ),
            Task(
                name=&amp;quot;Optimize DB&amp;quot;,
                description=&amp;quot;Optimize the database access layer.&amp;quot;,
                is_done=False,
            ),
            Task(
                name=&amp;quot;Install AppSignal&amp;quot;,
                description=&amp;quot;Install AppSignal to track errors.&amp;quot;,
                is_done=False,
            ),
        ]

        for task in tasks:
            db.session.add(task)

        db.session.commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;What&amp;#39;s Happening Here?&lt;/h4&gt;
&lt;p&gt;This script performs the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fetches Flask&amp;#39;s app instance.&lt;/li&gt;
&lt;li&gt;Creates the database and its structure via &lt;code&gt;db.create_all()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Populates the database with three sample tasks.&lt;/li&gt;
&lt;li&gt;Commits all the changes to the database via &lt;code&gt;db.session.commit()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h3&gt;Defining Views&lt;/h3&gt;
&lt;p&gt;Define the views in &lt;em&gt;app.py&lt;/em&gt; like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# app.py

@app.route(&amp;quot;/&amp;quot;)
def list_view():
    tasks = Task.query.all()
    return jsonify([task.as_dict() for task in tasks])


@app.route(&amp;quot;/&amp;lt;int:task_id&amp;gt;&amp;quot;, methods=[&amp;quot;GET&amp;quot;])
def detail_view(task_id):
    task = db.get_or_404(Task, task_id)
    return jsonify(task.as_dict())


@app.route(&amp;quot;/create&amp;quot;, methods=[&amp;quot;POST&amp;quot;])
def create_view():
    name = request.form.get(&amp;quot;name&amp;quot;, type=str)
    description = request.form.get(&amp;quot;description&amp;quot;, type=str)

    task = Task(name=name, description=description)
    db.session.add(task)
    db.session.commit()

    return jsonify(task.as_dict()), 201


@app.route(&amp;quot;/toggle-done/&amp;lt;int:task_id&amp;gt;&amp;quot;, methods=[&amp;quot;PATCH&amp;quot;])
def toggle_done_view(task_id):
    task = db.get_or_404(Task, task_id)

    task.is_done = not task.is_done
    db.session.commit()

    return jsonify(task.as_dict())


@app.route(&amp;quot;/delete/&amp;lt;int:task_id&amp;gt;&amp;quot;, methods=[&amp;quot;DELETE&amp;quot;])
def delete_view(task_id):
    task = db.get_or_404(Task, task_id)

    db.session.delete(task)
    db.session.commit()

    return jsonify({}), 204


@app.route(&amp;quot;/statistics&amp;quot;, methods=[&amp;quot;GET&amp;quot;])
def statistics_view():
    done_tasks_count = Task.query.filter_by(is_done=True).count()
    undone_tasks_count = Task.query.filter_by(is_done=False).count()
    done_percentage = done_tasks_count / (done_tasks_count + undone_tasks_count) * 100

    return jsonify({
        &amp;quot;done_tasks_count&amp;quot;: done_tasks_count,
        &amp;quot;undone_tasks_count&amp;quot;: undone_tasks_count,
        &amp;quot;done_percentage&amp;quot;: done_percentage,
    })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don&amp;#39;t forget about the import:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import jsonify, request
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;What&amp;#39;s Happening Here?&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;We define six API endpoints.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;list_view()&lt;/code&gt; fetches all the tasks, serializes and returns them.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;detail_view()&lt;/code&gt; fetches a specific task, serializes and returns it.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;create_view()&lt;/code&gt; creates a new task from the provided data.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toggle_done_view()&lt;/code&gt; toggles the task&amp;#39;s &lt;code&gt;is_done&lt;/code&gt; property.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;delete_view()&lt;/code&gt; deletes a specific task.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;statistics_view()&lt;/code&gt; calculates general app statistics.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Great, we&amp;#39;ve successfully created a simple TODO web app!&lt;/p&gt;
&lt;h2&gt;Test Your Python Flask App&amp;#39;s Errors with AppSignal&lt;/h2&gt;
&lt;p&gt;During the development of our web app, we intentionally left in some faulty code. We&amp;#39;ll now trigger these bugs to see what happens when an error occurs.&lt;/p&gt;
&lt;p&gt;Before proceeding, ensure your Flask development server is running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;(venv)$ flask run --debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your API should be accessible at &lt;a href=&quot;http://localhost:5000/&quot;&gt;http://localhost:5000/&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AppSignal should, of course, be employed when your application is in production rather than during development, as shown in this article.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Error 1: &lt;code&gt;OperationalError&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;To trigger the first error, request the task list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ curl --location &amp;#39;localhost:5000/&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will return an &lt;code&gt;Internal Server Error&lt;/code&gt;. Let&amp;#39;s use AppSignal to figure out what went wrong.&lt;/p&gt;
&lt;p&gt;Open your favorite web browser and navigate to your &lt;a href=&quot;https://appsignal.com/accounts&quot;&gt;AppSignal dashboard&lt;/a&gt;. Select your organization and then your application. Lastly, choose &amp;quot;Errors &amp;gt; Issue list&amp;quot; on the sidebar:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-05/appsignal-errors-issue-list.png&quot; alt=&quot;AppSignal Errors Issue List&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll see that an &lt;code&gt;OperationalError&lt;/code&gt; was reported. Click on it to inspect it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-05/appsignal-errors-issue-details.png&quot; alt=&quot;AppSignal Errors Issue Details&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The error detail page will display the error message, backtrace, state, trends, and so on.&lt;/p&gt;
&lt;p&gt;We can figure out what went wrong just by looking at the error message. &lt;code&gt;no such table: task&lt;/code&gt; tells us that we forgot to initialize the database.&lt;/p&gt;
&lt;p&gt;To fix that, run the previously created script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python init_db.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Retest the app and mark the issue as &amp;quot;Closed&amp;quot; once you&amp;#39;ve verified everything works.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-05/appsignal-errors-mark-as-closed.png&quot; alt=&quot;AppSignal Errors Issue Tag Closed&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Error 2: &lt;code&gt;IntegrityError&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s trigger the next error by trying to create a task without a &lt;code&gt;name&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ curl --location &amp;#39;localhost:5000/create&amp;#39; \
       --form &amp;#39;description=&amp;quot;Test the web application.&amp;quot;&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open the AppSignal dashboard and navigate to the &lt;code&gt;IntegrityError&lt;/code&gt;&amp;#39;s details.&lt;/p&gt;
&lt;p&gt;Now instead of just checking the error message, select &amp;quot;Samples&amp;quot; in the navigation:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-05/appsignal-errors-samples.png&quot; alt=&quot;AppSignal Errors Samples&quot;/&gt;&lt;/p&gt;
&lt;p&gt;A sample refers to a recorded instance of a specific error. Select the first sample.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-05/appsignal-errors-sample-details.png&quot; alt=&quot;AppSignal Errors Sample Details&quot;/&gt;&lt;/p&gt;
&lt;p&gt;By checking the backtrace, we can see exactly what line caused the error. As you can see, the error happened in &lt;em&gt;app.py&lt;/em&gt; on line 53 when we tried saving the task to the database.&lt;/p&gt;
&lt;p&gt;To fix it, provide a &lt;code&gt;default&lt;/code&gt; when assigning the &lt;code&gt;name&lt;/code&gt; variable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# app.py

@app.route(&amp;quot;/create&amp;quot;, methods=[&amp;quot;POST&amp;quot;])
def create_view():
    name = request.form.get(&amp;quot;name&amp;quot;, type=str, default=&amp;quot;Unnamed Task&amp;quot;)  # new
    description = request.form.get(&amp;quot;description&amp;quot;, type=str, default=&amp;quot;&amp;quot;)

    task = Task(name=name, description=description)
    db.session.add(task)
    db.session.commit()

    return jsonify(task.as_dict()), 201
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Error 3: &lt;code&gt;ZeroDivisionError&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Now we&amp;#39;ll delete all tasks and then calculate the statistics by running the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ curl --location --request DELETE &amp;#39;localhost:5000/delete/1&amp;#39;
$ curl --location --request DELETE &amp;#39;localhost:5000/delete/2&amp;#39;
$ curl --location --request DELETE &amp;#39;localhost:5000/delete/3&amp;#39;
$ curl --location --request DELETE &amp;#39;localhost:5000/delete/4&amp;#39;
$ curl --location --request DELETE &amp;#39;localhost:5000/delete/5&amp;#39;
$ curl --location &amp;#39;localhost:5000/statistics&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As expected, a &lt;code&gt;ZeroDivisonError&lt;/code&gt; is raised. To track the error, follow the same approach as described in the previous section.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-05/appsignal-errors-sample-backtrace.png&quot; alt=&quot;AppSignal ZeroDivisionError Sample&quot;/&gt;&lt;/p&gt;
&lt;p&gt;To fix it, add a zero check to the &lt;em&gt;statistics_view()&lt;/em&gt; endpoint like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# app.py

@app.route(&amp;quot;/statistics&amp;quot;, methods=[&amp;quot;GET&amp;quot;])
def statistics_view():
    done_tasks_count = Task.query.filter_by(is_done=True).count()
    undone_tasks_count = Task.query.filter_by(is_done=False).count()

    # new
    if done_tasks_count + undone_tasks_count == 0:
        done_percentage = 0
    else:
        done_percentage = done_tasks_count / \
                          (done_tasks_count + undone_tasks_count) * 100

    return jsonify({
        &amp;quot;done_tasks_count&amp;quot;: done_tasks_count,
        &amp;quot;undone_tasks_count&amp;quot;: undone_tasks_count,
        &amp;quot;done_percentage&amp;quot;: done_percentage,
    })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Retest the endpoint and mark it as &amp;quot;Closed&amp;quot; once you&amp;#39;ve verified the issue has been resolved.&lt;/p&gt;
&lt;h2&gt;Manual Tracking&lt;/h2&gt;
&lt;p&gt;By default, errors are only reported to AppSignal when exceptions are left unhandled. However, in some cases, you may want handled exceptions to be reported.&lt;/p&gt;
&lt;p&gt;To accomplish this, you can utilize AppSignal&amp;#39;s helper methods:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentation/exception-handling.html#set_error&quot;&gt;&lt;code&gt;set_error()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentation/exception-handling.html#send_error&quot;&gt;&lt;code&gt;send_error()&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://docs.appsignal.com/python/instrumentation/exception-handling.html#additional-metadata&quot;&gt;&lt;code&gt;send_error_with_context()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, we&amp;#39;ve covered how to monitor errors in a Flask app using AppSignal.&lt;/p&gt;
&lt;p&gt;We explored two error reporting methods: automatic tracking and manual tracking (using helper methods). With this knowledge, you can easily incorporate AppSignal into your Flask projects.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;https://blog.appsignal.com/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post!&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Track Errors in FastAPI for Python with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/03/27/track-errors-in-fastapi-for-python-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2024/03/27/track-errors-in-fastapi-for-python-with-appsignal.html</id>
    <published>2024-03-27T00:00:00+00:00</published>
    <updated>2024-03-27T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll make use of AppSignal to track errors in FastAPI.</summary>
    <content type="html">&lt;p&gt;When you first try a new library or framework, you are excited about it.
However, as soon as you run something on production, things are less than ideal — an error here, an exception there - bugs everywhere! You start reading your logs, but you often lack context, like how often an error happens, in what line, etc.&lt;/p&gt;
&lt;p&gt;Fortunately, tools such as AppSignal can help. AppSignal helps you track your errors and gives you a lot of valuable insights. For example, you can very quickly see how often an error happens, in what line, and for which deployment.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s what we&amp;#39;ll do now: leverage AppSignal to track errors in a FastAPI application.&lt;/p&gt;
&lt;h2&gt;Setting Up Our FastAPI for Python Project&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s prepare a project we&amp;#39;ll use as an example. First, create a new directory and virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ mkdir fastapi_appsignal
$ cd fastapi_appsignal
$ python3.12 -m venv venv
$ source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second, install FastAPI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ pip install &amp;quot;fastapi[all]&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Third, add a new file called &lt;code&gt;main.py&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import uuid
from uuid import UUID

from fastapi import FastAPI
from pydantic import BaseModel


class TodoData(BaseModel):
    title: str
    done: bool = False


class Todo(BaseModel):
    id: UUID
    title: str
    done: bool = False


TODOS = []
app = FastAPI()


@app.get(&amp;quot;/todos&amp;quot;)
def todo_list() -&amp;gt; list[Todo]:
    return TODOS


@app.post(&amp;quot;/todos&amp;quot;)
def todo_create(data: TodoData) -&amp;gt; Todo:
    todo = Todo(id=uuid.uuid4(), title=data.title, done=data.done)
    TODOS.append(todo)
    return todo


@app.post(&amp;quot;/todos/{todo_id}&amp;quot;)
def todo_edit(todo_id: UUID, data: TodoData) -&amp;gt; Todo:
    todo = next((t for t in TODOS if t.id == todo_id))
    todo.title = data.title
    todo.done = data.done
    return todo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What we have here is a very simple API example for managing TODOs. We&amp;#39;ll use this API to demonstrate how to track errors with AppSignal.&lt;/p&gt;
&lt;h2&gt;Configure AppSignal with FastAPI&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;If you don&amp;#39;t have an AppSignal account, &lt;a href=&quot;https://appsignal.com/users/sign_up&quot;&gt;create one - AppSignal offers a 30 day free trial&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;First, we need to install &lt;code&gt;appsignal&lt;/code&gt; and &lt;code&gt;opentelemetry-instrumentation-fastapi&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ pip install appsignal
$ pip install opentelemetry-instrumentation-fastapi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second, let&amp;#39;s configure AppSignal for FastAPI. Create a new file called &lt;code&gt;__appsignal__.py&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name=&amp;quot;fastapi_appsignal&amp;quot;,
    push_api_key=os.getenv(&amp;quot;APPSIGNAL_PUSH_API_KEY&amp;quot;),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;Third, update the &lt;code&gt;main.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import uuid
from uuid import UUID

from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor  # new
from pydantic import BaseModel

from __appsignal__ import appsignal  # new

appsignal.start()  # new


class TodoData(BaseModel):
    title: str
    done: bool = False


class Todo(BaseModel):
    id: UUID
    title: str
    done: bool = False


TODOS = []
app = FastAPI()


@app.get(&amp;quot;/todos&amp;quot;)
def todo_list() -&amp;gt; list[Todo]:
    return TODOS


@app.post(&amp;quot;/todos&amp;quot;)
def todo_create(data: TodoData) -&amp;gt; Todo:
    todo = Todo(id=uuid.uuid4(), title=data.title, done=data.done)
    TODOS.append(todo)
    return todo


@app.post(&amp;quot;/todos/{todo_id}&amp;quot;)
def todo_edit(todo_id: UUID, data: TodoData) -&amp;gt; Todo:
    todo = next((t for t in TODOS if t.id == todo_id))
    todo.title = data.title
    todo.done = data.done
    return todo


FastAPIInstrumentor().instrument_app(app)  # new
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to start the AppSignal agent alongside our FastAPI application. For more details, refer to the &lt;a href=&quot;https://docs.appsignal.com/python/installation&quot;&gt;AppSignal Python installation docs&lt;/a&gt; and &lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/fastapi.html&quot;&gt;FastAPI instrumentation docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At this point, our project is ready to track our first error with AppSignal. To send errors to AppSignal, you need to get your push API key. You can get one by following the &lt;a href=&quot;https://docs.appsignal.com/guides/new-application.html&quot;&gt;official AppSignal guide&lt;/a&gt; for adding a new app. After you have your push API key, start the server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ export APPSIGNAL_PUSH_API_KEY=&amp;lt;your_appsignal_push_api_key&amp;gt;
$ uvicorn main:app --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your server is up and running, open a new terminal and send a request to the API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -X POST -H &amp;quot;Content-Type: application/json&amp;quot; -d &amp;#39;{&amp;quot;title&amp;quot;: &amp;quot;Buy milk&amp;quot;, &amp;quot;done&amp;quot;: true}&amp;#39; http://localhost:8000/todos/dde18df8-7e11-4f84-bcbf-2eee1a5d157e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This request will raise the &lt;code&gt;StopIteration&lt;/code&gt; exception inside the &lt;code&gt;todo_edit&lt;/code&gt; function. That&amp;#39;s because we use the &lt;code&gt;next&lt;/code&gt; function to get the TODO with the given ID. The exception is raised since we&amp;#39;re using the UUID of a non-existing task.&lt;/p&gt;
&lt;p&gt;To confirm that AppSignal is working, click on &lt;em&gt;Next Step&lt;/em&gt; at the bottom of the page from which you&amp;#39;ve copied the push API key.
&lt;img src=&quot;/images/blog/2024-03/next_step.png&quot; alt=&quot;Next Step&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Once you&amp;#39;ve clicked on &lt;em&gt;Next Step&lt;/em&gt;, wait for AppSignal to confirm that it has received data from your app. After this, you can open the &lt;em&gt;Errors -&amp;gt; Issue list&lt;/em&gt; tab and see the error we&amp;#39;ve just raised.
&lt;img src=&quot;/images/blog/2024-03/issue_list.png&quot; alt=&quot;Issue List&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Congrats! You&amp;#39;ve just tracked your first error in FastAPI with AppSignal. But we can do better than that. Let&amp;#39;s see how we can get more insights into our errors.&lt;/p&gt;
&lt;h2&gt;Get More Error Insights&lt;/h2&gt;
&lt;p&gt;At this point, we&amp;#39;re able to track errors with AppSignal. On the dashboard, you can see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How often an error happens&lt;/li&gt;
&lt;li&gt;Error messages&lt;/li&gt;
&lt;li&gt;Error backtraces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also add notes to errors, assign errors, and post messages to Slack when errors occur.&lt;/p&gt;
&lt;p&gt;To get even more insights, you can set up &lt;a href=&quot;https://docs.appsignal.com/application/markers/deploy-markers.html&quot;&gt;deploy markers&lt;/a&gt; and &lt;a href=&quot;https://docs.appsignal.com/application/backtrace-links.html&quot;&gt;backtrace links&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Using them together lets you jump directly to the source code on your GitHub repository from the error&amp;#39;s backtrace. For this to work, you need to push your code to GitHub. Do that before going further (you can follow the &lt;a href=&quot;https://docs.github.com/en/migrations/importing-source-code/using-the-command-line-to-import-source-code/adding-locally-hosted-code-to-github#adding-a-local-repository-to-github-using-git&quot;&gt;official guide&lt;/a&gt;). Once this is done, we can continue setting up deploy markers and backtrace links.&lt;/p&gt;
&lt;h3&gt;Deploy Markers&lt;/h3&gt;
&lt;p&gt;First, we need to set up deploy markers. To do that, update the &lt;code&gt;__appsignal__.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

from appsignal import Appsignal

appsignal = Appsignal(
    active=True,
    name=&amp;quot;fastapi_appsignal&amp;quot;,
    push_api_key=os.getenv(&amp;quot;APPSIGNAL_PUSH_API_KEY&amp;quot;),
    revision=os.getenv(&amp;quot;APPSIGNAL_REVISION&amp;quot;),  # new
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We enable deploy markers by setting the &lt;code&gt;revision&lt;/code&gt; value to a non-null value. Setting up deploy markers will allow us to see which deployment causes an error.
We&amp;#39;ll set &lt;code&gt;APPSIGNAL_REVISION&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; because that&amp;#39;s the name of our branch.
In the production setup, you should set this to the commit hash of the deployed version or tag name.&lt;/p&gt;
&lt;p&gt;Second, we need to enable backtrace links. We already enabled deploy markers, so we&amp;#39;re halfway there.
What&amp;#39;s left is to link our GitHub repository. To do that, go to your organization&amp;#39;s settings and click on &lt;em&gt;Install GitHub App&lt;/em&gt;.
&lt;img src=&quot;/images/blog/2024-03/install_github_app.png&quot; alt=&quot;Install GitHub App&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Once on GitHub, select your organization.
&lt;img src=&quot;/images/blog/2024-03/select_github_organization.png&quot; alt=&quot;Select Organization&quot;/&gt;&lt;/p&gt;
&lt;p&gt;After that, keep everything as it is and click &lt;em&gt;Install&lt;/em&gt;.
&lt;img src=&quot;/images/blog/2024-03/install_app.png&quot; alt=&quot;Install App&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll be redirected back to AppSignal&amp;#39;s dashboard. Select the repository you want to link to your app and click &lt;em&gt;Save repository selection&lt;/em&gt;.
&lt;img src=&quot;/images/blog/2024-03/select_repository_link.png&quot; alt=&quot;Select Repository&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s raise another error to test that everything is working as expected. To do that, set environment variables and start your server again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ export APPSIGNAL_PUSH_API_KEY=&amp;lt;your_appsignal_push_api_key&amp;gt;
$ export APPSIGNAL_REVISION=main
$ uvicorn main:app --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, open a new terminal and send another request to the API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -X POST -H &amp;quot;Content-Type: application/json&amp;quot; -d &amp;#39;{&amp;quot;title&amp;quot;: &amp;quot;Buy milk&amp;quot;, &amp;quot;done&amp;quot;: true}&amp;#39; http://localhost:8000/todos/dde18df8-7e11-4f84-bcbf-2eee1a5d157e
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Backtrace Links&lt;/h3&gt;
&lt;p&gt;Once the latest error is tracked, you can try backtrace links. Go to the &lt;em&gt;Errors -&amp;gt; Issue list&lt;/em&gt; tab and click on the error we&amp;#39;ve just raised. Once on the error&amp;#39;s detail page, click &lt;em&gt;Inspect latest example&lt;/em&gt;.
&lt;img src=&quot;/images/blog/2024-03/inspect_latest_example.png&quot; alt=&quot;Inspect Latest Example&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll end up on the details of the latest error sample, where you&amp;#39;ll see the &lt;em&gt;Backtrace&lt;/em&gt; section. On the right side of the backtrace, you&amp;#39;ll see a link to the source code on GitHub - &lt;em&gt;git&lt;/em&gt;.
&lt;img src=&quot;/images/blog/2024-03/backtrace_link.png&quot; alt=&quot;Backtrace link&quot;/&gt;&lt;/p&gt;
&lt;p&gt;When you click on &lt;em&gt;git&lt;/em&gt;, you&amp;#39;ll be redirected to the source code on GitHub. You&amp;#39;ll see the exact line where the error happened.
Now, you can easily pinpoint the problematic code and fix the issue. This is way less work than parsing log files and doing things manually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: If you&amp;#39;re not pointed to the correct line of code on GitHub, make sure you&amp;#39;ve committed and pushed the latest version of your code to GitHub.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Reporting Python FastAPI Errors Explicitly&lt;/h2&gt;
&lt;p&gt;So far, we&amp;#39;ve seen how to track errors raised by our FastAPI application. But we might want to report errors explicitly in some cases.
For example, we might use an AI API to generate emails for our clients.&lt;/p&gt;
&lt;p&gt;As you may know, these APIs are often unreliable, and seeing internal server errors is quite common.
In such cases, we want to handle the error - for example, by falling back to a default template.&lt;/p&gt;
&lt;p&gt;We want to report an error to AppSignal to see how often it happens and what the error message is.&lt;/p&gt;
&lt;h3&gt;Using &lt;code&gt;set_error&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In some cases, we can make usage more robust (e.g., by implementing retries) or contact the API provider&amp;#39;s support.
To handle such scenarios, AppSignal provides the &lt;code&gt;set_error&lt;/code&gt; function. Let&amp;#39;s see how to use it.&lt;/p&gt;
&lt;p&gt;Update your &lt;code&gt;main.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ... existing imports
from appsignal import set_error  # new
from fastapi import FastAPI, HTTPException  # new
# ... existing imports


# ... existing code

@app.post(&amp;quot;/todos/{todo_id}&amp;quot;)
def todo_edit(todo_id: UUID, data: TodoData) -&amp;gt; Todo:
    try:  # new
        todo = next((t for t in TODOS if t.id == todo_id))
    except StopIteration:
        set_error(Exception(&amp;quot;Todo not found&amp;quot;))
        raise HTTPException(status_code=404, detail=&amp;quot;Todo not found&amp;quot;)
    todo.title = data.title
    todo.done = data.done
    return todo


# ... existing code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our case, users won&amp;#39;t see the &lt;em&gt;Internal server error&lt;/em&gt; anymore. Instead, we&amp;#39;ll return the &lt;em&gt;Todo not found&lt;/em&gt; error.
That&amp;#39;s better for our users and our API. Without &lt;code&gt;set_error&lt;/code&gt;, nothing would be reported to AppSignal.
With &lt;code&gt;set_error&lt;/code&gt;, the error is reported and tracked as before. Hence, we&amp;#39;ll see how often this error happens and the error message.
Backtrace links still apply, as in the previous example.&lt;/p&gt;
&lt;p&gt;To test that everything is working as expected, follow the same steps as above:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start the server.&lt;/li&gt;
&lt;li&gt;Send a request to the API.&lt;/li&gt;
&lt;li&gt;Go to the dashboard &lt;em&gt;Errors -&amp;gt; Issue list&lt;/em&gt; in AppSignal and check the new error.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&amp;#39;s all great, but sometimes, we need more context when manually reporting an error to AppSignal.
In such cases, we can use the &lt;code&gt;send_error_with_context&lt;/code&gt; function. This function allows us to manually add needed context to the error - e.g., the ID of a non-existing task.
Once again, update the &lt;code&gt;main.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# ... existing imports
from appsignal import send_error_with_context, set_params  # new
from fastapi import FastAPI, HTTPException  # new
# ... existing imports


# ... existing code

@app.post(&amp;quot;/todos/{todo_id}&amp;quot;)
def todo_edit(todo_id: UUID, data: TodoData) -&amp;gt; Todo:
    try:
        todo = next((t for t in TODOS if t.id == todo_id))
    except StopIteration:
        with send_error_with_context(Exception(&amp;quot;Todo not found&amp;quot;)):
            set_params({&amp;quot;todo_id&amp;quot;: todo_id})
        raise HTTPException(status_code=404, detail=&amp;quot;Todo not found&amp;quot;)
    todo.title = data.title
    todo.done = data.done
    return todo



# ... existing code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To test that everything works as expected, follow the same steps as above.&lt;/p&gt;
&lt;p&gt;In this case, you&amp;#39;ll also see the &lt;em&gt;Parameters&lt;/em&gt; section that contains the parameters you&amp;#39;ve explicitly set (here, that&amp;#39;s the ID of the non-existing task).
&lt;img src=&quot;/images/blog/2024-03/error_with_context.png&quot; alt=&quot;Error with context&quot;/&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;#39;s it!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this blog post, we&amp;#39;ve seen how to track errors in FastAPI with AppSignal. We set up AppSignal for FastAPI, tracked errors, and gained some further insights.&lt;/p&gt;
&lt;p&gt;Besides that, we also learned how to explicitly track errors using the AppSignal functions &lt;code&gt;set_error&lt;/code&gt; and &lt;code&gt;send_error_with_context&lt;/code&gt;. This should give you a good starting point to track your errors better and fix them faster from now on.&lt;/p&gt;
&lt;p&gt;Happy engineering!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Track Errors in Your Python Django Application with AppSignal</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/02/28/track-errors-in-your-python-django-application-with-appsignal.html"/>
    <id>https://blog.appsignal.com/2024/02/28/track-errors-in-your-python-django-application-with-appsignal.html</id>
    <published>2024-02-28T00:00:00+00:00</published>
    <updated>2024-02-28T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s see how we can use AppSignal to track and fix errors in a Django application.</summary>
    <content type="html">&lt;p&gt;In this post, we will specifically look at using AppSignal to track errors in a Django application.&lt;/p&gt;
&lt;p&gt;We&amp;#39;ll first create a Django project, install AppSignal, introduce some faulty code, and then use the AppSignal Errors dashboard to debug and resolve errors.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get started!&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;To follow along, you&amp;#39;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.8+&lt;/a&gt; installed on your local machine&lt;/li&gt;
&lt;li&gt;An &lt;a href=&quot;https://docs.appsignal.com/support/operating-systems.html&quot;&gt;AppSignal-supported operating system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;An &lt;a href=&quot;https://appsignal.com/users/sign_in&quot;&gt;AppSignal account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Basic Django knowledge&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll be working on a movie review web app. The app will allow us to manage movies and reviews via a RESTful API. To build it, we&amp;#39;ll utilize &lt;a href=&quot;https://www.djangoproject.com/&quot;&gt;Django&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I recommend you follow along with the movie review web app first. After you grasp the basic concepts, using AppSignal with your projects will be easy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Begin by creating and activating a new virtual environment, installing Django, and bootstrapping a new Django project.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you need help, refer to the &lt;a href=&quot;https://docs.djangoproject.com/en/4.2/intro/install/&quot;&gt;Quick install guide&lt;/a&gt; from the &lt;a href=&quot;https://docs.djangoproject.com/en/4.2/&quot;&gt;Django docs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; The source code for this project can be found on the &lt;a href=&quot;https://github.com/duplxey/appsignal-django-error-tracking&quot;&gt;appsignal-django-error-tracking&lt;/a&gt; GitHub repo.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Install AppSignal for Django&lt;/h2&gt;
&lt;p&gt;To add AppSignal to a Django-based project, follow the AppSignal documentation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/installation&quot;&gt;AppSignal Python installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentations/django.html&quot;&gt;Django instrumentation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To ensure everything works, start the development server. Your Django project will automatically send a demo error to AppSignal. In addition, you should receive an email notification.&lt;/p&gt;
&lt;p&gt;From now on, all your app errors will be reported to AppSignal.&lt;/p&gt;
&lt;h2&gt;App Logic&lt;/h2&gt;
&lt;p&gt;Moving along, let&amp;#39;s implement the movie review app logic.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You might notice that some code snippets contain faulty code. The defective code is placed there on purpose to later demonstrate how AppSignal error tracking works.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;First, create a dedicated app for movies and reviews:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python manage.py startapp movies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the newly-created app to &lt;code&gt;INSTALLED_APPS&lt;/code&gt; in &lt;em&gt;settings.py&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# core/settings.py

INSTALLED_APPS = [
    # ...
    &amp;#39;movies.apps.MoviesConfig&amp;#39;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Define the app&amp;#39;s database models in &lt;em&gt;models.py&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# movies/models.py

from django.db import models


class Movie(models.Model):
    title = models.CharField(max_length=128)
    description = models.TextField(max_length=512)
    release_year = models.IntegerField()

    def get_average_rating(self):
        reviews = MovieReview.objects.filter(movie=self)
        return sum(review.rating for review in reviews) / len(reviews)

    def serialize_to_json(self):
        return {
            &amp;#39;id&amp;#39;: self.id,
            &amp;#39;title&amp;#39;: self.title,
            &amp;#39;description&amp;#39;: self.description,
            &amp;#39;release_year&amp;#39;: self.release_year,
        }

    def __str__(self):
        return f&amp;#39;{self.title} ({self.release_year})&amp;#39;


class MovieReview(models.Model):
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
    rating = models.IntegerField()

    def save(self, force_insert=False, force_update=False, using=None, update=None):
        if self.rating &amp;lt; 1 or self.rating &amp;gt; 5:
            raise ValueError(&amp;#39;Rating must be between 1 and 5.&amp;#39;)

        super().save(force_insert, force_update, using, update)

    def serialize_to_json(self):
        return {
            &amp;#39;id&amp;#39;: self.id,
            &amp;#39;movie&amp;#39;: self.movie.serialize_to_json(),
            &amp;#39;rating&amp;#39;: self.rating,
        }

    def __str__(self):
        return f&amp;#39;{self.movie.title} - {self.rating}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What&amp;#39;s Happening Here?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;We define two models, &lt;code&gt;Movie&lt;/code&gt; and &lt;code&gt;MovieReview&lt;/code&gt;. A &lt;code&gt;Movie&lt;/code&gt; can have multiple &lt;code&gt;MovieReview&lt;/code&gt;s. Both models include a &lt;code&gt;serialize_to_json()&lt;/code&gt; method for serializing the object to JSON.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Movie&lt;/code&gt; model includes &lt;code&gt;get_average_rating()&lt;/code&gt; for calculating a movie&amp;#39;s average rating.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MovieReview&lt;/code&gt;&amp;#39;s &lt;code&gt;rating&lt;/code&gt; is locked in a &lt;code&gt;[1, 5]&lt;/code&gt; interval via the modified &lt;code&gt;save()&lt;/code&gt; method.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, make migrations and migrate the database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h2&gt;Define the Views in Python&lt;/h2&gt;
&lt;p&gt;Next, define the views in &lt;em&gt;movies/views.py&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# movies/views.py

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from movies.models import Movie, MovieReview


def index_view(request):
    queryset = Movie.objects.all()
    data = [movie.serialize_to_json() for movie in queryset]

    return JsonResponse(data, safe=False)


def statistics_view(request):
    queryset = Movie.objects.all()
    data = []

    for movie in queryset:
        data.append({
            &amp;#39;id&amp;#39;: movie.id,
            &amp;#39;title&amp;#39;: movie.title,
            &amp;#39;average_rating&amp;#39;: movie.get_average_rating(),
        })

    return JsonResponse(data, safe=False)


@csrf_exempt
@require_http_methods([&amp;quot;POST&amp;quot;])
def review_view(request):
    movie_id = request.POST.get(&amp;#39;movie&amp;#39;)
    rating = request.POST.get(&amp;#39;rating&amp;#39;)

    if (movie_id is None or rating is None) or \
            (not movie_id.isdigit() or not rating.isdigit()):
        return JsonResponse({
            &amp;#39;detail&amp;#39;: &amp;#39;Please provide a `movie` (int) and `rating` (int).&amp;#39;,
        }, status=400)

    movie_id = int(movie_id)
    rating = int(rating)

    try:
        movie = Movie.objects.get(id=movie_id)
        MovieReview.objects.create(
            movie=movie,
            rating=rating,
        )

        return JsonResponse({
            &amp;#39;detail&amp;#39;: &amp;#39;A review has been successfully posted&amp;#39;,
        })

    except Movie.DoesNotExist:
        return JsonResponse({
            &amp;#39;detail&amp;#39;: &amp;#39;Movie does not exist.&amp;#39;,
        }, status=400)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What&amp;#39;s Happening Here?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;We define three views: &lt;code&gt;index_view()&lt;/code&gt;, &lt;code&gt;statistics_view()&lt;/code&gt;, and &lt;code&gt;review_view()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;index_view()&lt;/code&gt; fetches all the movies, serializes them, and returns them.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;statistics_view()&lt;/code&gt; calculates and returns the average rating of every movie.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;review_view()&lt;/code&gt; allows users to create a movie review by providing the movie ID and a rating.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Register the URLs&lt;/h2&gt;
&lt;p&gt;The last thing we must do is take care of the URLs.&lt;/p&gt;
&lt;p&gt;Create a &lt;em&gt;urls.py&lt;/em&gt; file within the &lt;code&gt;movies&lt;/code&gt; app with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# movies/urls.py

from django.urls import path

from movies import views

urlpatterns = [
    path(&amp;#39;statistics/&amp;#39;, views.statistics_view, name=&amp;#39;movies-statistics&amp;#39;),
    path(&amp;#39;review/&amp;#39;, views.review_view, name=&amp;#39;movies-review&amp;#39;),
    path(&amp;#39;&amp;#39;, views.index_view, name=&amp;#39;movies-index&amp;#39;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then register the app URLs globally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# core/urls.py

from django.contrib import admin
from django.urls import path, include  # new import

urlpatterns = [
    path(&amp;#39;movies/&amp;#39;, include(&amp;#39;movies.urls&amp;#39;)),  # new
    path(&amp;#39;admin/&amp;#39;, admin.site.urls),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using Fixtures&lt;/h2&gt;
&lt;p&gt;To get some test data to work with, I&amp;#39;ve prepared two fixtures.&lt;/p&gt;
&lt;p&gt;First, &lt;a href=&quot;https://github.com/duplxey/appsignal-django-error-tracking/tree/master/fixtures&quot;&gt;download the fixtures&lt;/a&gt;, create a &lt;em&gt;fixtures&lt;/em&gt; folder in the project root, and place them in there:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;django-error-tracking/
+-- fixtures/
    +-- Movie.json
    +-- MovieReview.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, run the following two commands to load them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python manage.py loaddata fixtures/Movie.json --app app.Movie
(venv)$ python manage.py loaddata fixtures/MovieReview.json --app app.MovieReview
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now have a functional API with some sample data to work with. In the next section, we&amp;#39;ll test it.&lt;/p&gt;
&lt;h2&gt;Test Your Django App&amp;#39;s Errors with AppSignal&lt;/h2&gt;
&lt;p&gt;During the development of our web app, we left intentional bugs in the code. We will now deliberately trigger these bugs to see what happens when an error occurs.&lt;/p&gt;
&lt;p&gt;Before proceeding, ensure your Django development server is running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your API should be accessible at &lt;a href=&quot;http://localhost:8000/movies&quot;&gt;http://localhost:8000/movies&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AppSignal should, of course, be employed when your application is in production rather than during development, as shown in this article.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Error 1: &lt;code&gt;ZeroDivisionError&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Start by visiting &lt;a href=&quot;http://localhost:8000/movies/statistics&quot;&gt;http://localhost:8000/movies/statistics&lt;/a&gt; in your favorite browser. This endpoint is supposed to calculate and return average movie ratings, but it results in a &lt;code&gt;ZeroDivisonError&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s use AppSignal to figure out what went wrong.&lt;/p&gt;
&lt;p&gt;First, navigate to your &lt;a href=&quot;https://appsignal.com/accounts&quot;&gt;AppSignal dashboard&lt;/a&gt; and select your application. On the sidebar, you&amp;#39;ll see several categories. Select &amp;quot;Errors &amp;gt; Issue list&amp;quot;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-02/django-issue-list.png&quot; alt=&quot;AppSignal Issue List&quot;/&gt;&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll see that a &lt;code&gt;ZeroDivisionError&lt;/code&gt; has been reported. Click on it to inspect it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-02/django-division-by-zero.png&quot; alt=&quot;AppSignal ValueError Issue&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The error detail page displays the error&amp;#39;s summary, trends, state, severity, etc. But we are interested in the samples. Select the &amp;quot;Samples&amp;quot; menu item in the navigation bar to access them.&lt;/p&gt;
&lt;p&gt;A sample refers to a recorded instance of a specific error. Select the first sample.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-02/django-division-by-zero-sample.png&quot; alt=&quot;AppSignal ValueError Issue Sample&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The sample&amp;#39;s detail page shows the error message, what device triggered the error, the backtrace, and more. We can look at the backtrace to determine what line of code caused the error.&lt;/p&gt;
&lt;p&gt;In the backtrace, we can see that the error occurred in &lt;em&gt;movies/models.py&lt;/em&gt; on line &lt;code&gt;16&lt;/code&gt;. Looking at the code, the error is obvious: if a movie has no reviews, this results in a division by zero.&lt;/p&gt;
&lt;p&gt;To fix this error, all you have to do is slightly modify the &lt;code&gt;Movie&lt;/code&gt;&amp;#39;s &lt;code&gt;get_average_rating()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# movies/models.py

class Movie(models.Model):
    # ...

    def get_average_rating(self):
        reviews = MovieReview.objects.filter(movie=self)

        # new condition
        if len(reviews) == 0:
            return 0

        return sum(review.rating for review in reviews) / len(reviews)

    # ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reload the development server, test the functionality again, and set the error&amp;#39;s state to &amp;quot;Closed&amp;quot; if everything works as expected.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-02/django-tag-incident-as-closed.png&quot; alt=&quot;AppSignal ValueError Issue Close&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;Error 2: &lt;code&gt;ValueError&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;We can trigger another error by submitting a review with a rating outside the &lt;code&gt;[1, 5]&lt;/code&gt; interval. To try it out, open your terminal and run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ curl --location --request POST &amp;#39;http://localhost:8000/movies/review/&amp;#39; \
     --form &amp;#39;movie=2&amp;#39; \
     --form &amp;#39;rating=6&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As expected, a &lt;code&gt;ValueError&lt;/code&gt; is reported to AppSignal. To track the error, follow the same procedure as in the previous section.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-02/django-value-error.png&quot; alt=&quot;AppSignal ValueError Details&quot;/&gt;&lt;/p&gt;
&lt;p&gt;The error can be easily fixed by adding a check to &lt;code&gt;review_view()&lt;/code&gt; in &lt;em&gt;views.py&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# movies/views.py

from django.core.validators import MinValueValidator, MaxValueValidator

# ...

@csrf_exempt
@require_http_methods([&amp;quot;POST&amp;quot;])
def review_view(request):
    # ...

    movie_id = int(movie_id)
    rating = int(rating)

    if rating &amp;lt; 1 or rating &amp;gt; 5:
        return JsonResponse({
            &amp;#39;detail&amp;#39;: &amp;#39;Rating must be between 1 and 5.&amp;#39;,
        }, status=400)

    try:
        movie = Movie.objects.get(id=movie_id)
        MovieReview.objects.create(
            movie=movie,
            rating=rating,
        )
        # ...

    # ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, test the functionality again and tag the error as &amp;quot;Resolved&amp;quot; if everything works as expected.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this article, you&amp;#39;ve learned how to use AppSignal to track errors in a Django application.&lt;/p&gt;
&lt;p&gt;To get the most out of AppSignal for Python, I suggest you review the following two resources:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/configuration.html&quot;&gt;AppSignal&amp;#39;s Python configuration docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.appsignal.com/python/instrumentation/exception-handling.html&quot;&gt;AppSignal&amp;#39;s Python exception handling docs&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>An Introduction to Testing with Django for Python</title>
    <link rel="alternate" href="https://blog.appsignal.com/2024/01/31/an-introduction-to-testing-with-django-for-python.html"/>
    <id>https://blog.appsignal.com/2024/01/31/an-introduction-to-testing-with-django-for-python.html</id>
    <published>2024-01-31T00:00:00+00:00</published>
    <updated>2024-01-31T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">We&#039;ll find out how to test a Django application with both unittest and pytest.</summary>
    <content type="html">&lt;p&gt;In a world of ever-changing technology, testing is an integral part of writing robust and reliable software.
Tests verify that your code behaves as expected, make it easier to maintain and refactor code, and serve as
documentation for your code.&lt;/p&gt;
&lt;p&gt;There are two widely used testing frameworks for testing Django applications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Django&amp;#39;s built-in test framework, built on Python&amp;#39;s unittest&lt;/li&gt;
&lt;li&gt;Pytest, combined with pytest-django&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this article, we will see how both work.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get started!&lt;/p&gt;
&lt;h2&gt;What Will We Test?&lt;/h2&gt;
&lt;p&gt;All the tests we&amp;#39;ll observe will use the same code, a &lt;em&gt;BookList&lt;/em&gt; endpoint.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Book&lt;/code&gt; model has &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, and &lt;code&gt;date_published&lt;/code&gt; fields.
Note that the default ordering is set by &lt;code&gt;date_published&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# models.py
class Book(models.Model):
    title = models.CharField(max_length=20)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, blank=True, null=True)
    date_published = models.DateField(null=True, blank=True)

    class Meta:
        ordering = [&amp;#39;date_published&amp;#39;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The view is just a simple &lt;code&gt;ListView&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# views.py
from django.views.generic import ListView
from .models import Book


class BookListView(ListView):
    model = Book
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the test examples will test the endpoint, we also need the URL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path(&amp;quot;books/&amp;quot;, views.BookListView.as_view(), name=&amp;quot;book_list&amp;quot;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now for the tests.&lt;/p&gt;
&lt;h2&gt;Unittest for Python&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/3/library/unittest.html&quot;&gt;Unittest&lt;/a&gt; is Python&amp;#39;s built-in testing framework. Django extends it with &lt;a href=&quot;https://docs.djangoproject.com/en/4.2/topics/testing/&quot;&gt;some of its own functionality&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Initially, you might be confused about what methods belong to unittest and what are Django&amp;#39;s extensions.&lt;/p&gt;
&lt;p&gt;Unittest provides:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/unittest.html#test-cases&quot;&gt;TestCase&lt;/a&gt;, serving as a base class.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setUp()&lt;/code&gt; and &lt;code&gt;tearDown()&lt;/code&gt; methods for the code you want to execute before or after each test method.
&lt;code&gt;setUpClass()&lt;/code&gt; and &lt;code&gt;tearDownClass()&lt;/code&gt; also run once per whole &lt;code&gt;TestClass&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Multiple types of &lt;a href=&quot;https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual&quot;&gt;assertions&lt;/a&gt;, such
as &lt;code&gt;assertEqual&lt;/code&gt;, &lt;code&gt;assertAlmostEqual&lt;/code&gt;, and &lt;code&gt;assertIsInstance&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The core part of the Django testing framework is &lt;a href=&quot;https://docs.djangoproject.com/en/4.2/topics/testing/tools/#testcase&quot;&gt;TestCase&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since it subclasses &lt;code&gt;unittest.TestCase&lt;/code&gt;, &lt;code&gt;TestCase&lt;/code&gt; has all the same functionality but also builds on top of it, with:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.djangoproject.com/en/4.2/topics/testing/tools/#django.test.TestCase.setUpTestData&quot;&gt;setUpTestData class method&lt;/a&gt;: This creates data once per &lt;code&gt;TestCase&lt;/code&gt; (unlike &lt;code&gt;setUp&lt;/code&gt;, which creates test data once per test).
Using &lt;code&gt;setUpTestData&lt;/code&gt; can significantly speed up your tests.&lt;/li&gt;
&lt;li&gt;Test data loaded via &lt;a href=&quot;https://docs.djangoproject.com/en/4.2/topics/testing/tools/#fixture-loading&quot;&gt;fixtures&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Django-specific assertions, such as &lt;code&gt;assertQuerySetEqual&lt;/code&gt;, &lt;code&gt;assertFormError&lt;/code&gt;, and &lt;code&gt;assertContains&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Temporary &lt;a href=&quot;https://docs.djangoproject.com/en/4.2/topics/testing/tools/#overriding-settings&quot;&gt;override settings&lt;/a&gt; you can set up during a test run.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Django&amp;#39;s testing framework also provides a &lt;a href=&quot;https://docs.djangoproject.com/en/4.2/topics/testing/tools/#the-test-client&quot;&gt;Client&lt;/a&gt; that doesn&amp;#39;t rely on &lt;code&gt;TestCase&lt;/code&gt; and so can be used with pytest.
&lt;code&gt;Client&lt;/code&gt; serves as a dummy browser, allowing users to create &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;POST&lt;/code&gt; requests.&lt;/p&gt;
&lt;h3&gt;How Tests Are Built with Unittest&lt;/h3&gt;
&lt;p&gt;Unittest supports test discovery, but the following rules must be followed:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The test should be in a file named with a &lt;code&gt;test&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;The test must be within a class that subclasses &lt;code&gt;unittest.TestCase&lt;/code&gt; (that includes using &lt;code&gt;django.TestCase&lt;/code&gt;). Including &amp;#39;Test&amp;#39; in the class name is not mandatory.&lt;/li&gt;
&lt;li&gt;The name of the test method must start with &lt;code&gt;test_&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;#39;s see what tests written with unittest look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# tests.py
from datetime import date

from django.test import TestCase
from django.urls import reverse
from .models import Book, Author

class BookListTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        author = Author.objects.create(first_name=&amp;#39;Jane&amp;#39;, last_name=&amp;#39;Austen&amp;#39;)
        cls.second_book = Book.objects.create(title=&amp;#39;Emma&amp;#39;, author=author, date_published=date(1815, 1, 1))
        cls.first_book = Book.objects.create(title=&amp;#39;Pride and Prejudice&amp;#39;, author=author, date_published=date(1813, 1, 1))

    def test_book_list_returns_all_books(self):
        response = self.client.get(reverse(&amp;#39;book_list&amp;#39;))
        response_book_list = response.context[&amp;#39;book_list&amp;#39;]

        self.assertEqual(response.status_code, 200)
        self.assertIn(self.first_book, response_book_list)
        self.assertIn(self.second_book, response_book_list)
        self.assertEqual(len(response_book_list), 2)

    def test_book_list_ordered_by_date_published(self):
        response = self.client.get(reverse(&amp;#39;book_list&amp;#39;))
        books = list(response.context[&amp;#39;book_list&amp;#39;])

        self.assertEqual(response.status_code, 200)
        self.assertQuerySetEqual(books, [self.first_book, self.second_book])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You must always put your test functions inside a class that extends a &lt;code&gt;TestCase&lt;/code&gt; class.
Although it&amp;#39;s not required, we&amp;#39;ve included &amp;#39;Test&amp;#39; in our class name so it&amp;#39;s easily recognizable as a test class at first glance.
Notice that the &lt;code&gt;TestCase&lt;/code&gt; is imported from Django, not unittest — you want access to additional Django functionality.&lt;/p&gt;
&lt;h3&gt;What&amp;#39;s Happening Here?&lt;/h3&gt;
&lt;p&gt;This &lt;code&gt;TestCase&lt;/code&gt; aims to check whether the &lt;code&gt;book_list&lt;/code&gt; endpoint works correctly.
There are two tests — one checks that all &lt;code&gt;Book&lt;/code&gt; objects in the database are included in the response as &lt;code&gt;book_list&lt;/code&gt;, and the second checks that the books are listed by ascending publishing date.&lt;/p&gt;
&lt;p&gt;Although the tests appear similar, they evaluate two distinct functionalities, which should not be combined into a single test.&lt;/p&gt;
&lt;p&gt;Since both tests need the same data in the database, we use the class method &lt;code&gt;setUpTestData&lt;/code&gt; for preparing data — both tests will have access to it without repeating the process twice.
Unlike the two &lt;code&gt;Book&lt;/code&gt; objects, the &lt;code&gt;Author&lt;/code&gt; object is not needed outside the setup method, so we don&amp;#39;t set it as an instance attribute.
I switched the order of the two books to ensure that the correct order isn&amp;#39;t accidental.&lt;/p&gt;
&lt;p&gt;The two tests look very similar — they both make a &lt;code&gt;GET&lt;/code&gt; request to the same URL and extract the &lt;code&gt;book_list&lt;/code&gt; from &lt;code&gt;context&lt;/code&gt;.
Methods inside the &lt;code&gt;TestCase&lt;/code&gt; class automatically have access to &lt;code&gt;client&lt;/code&gt;, which is used to make a request.&lt;/p&gt;
&lt;p&gt;Up to this point, both tests did the same thing — but the assertions differ.&lt;/p&gt;
&lt;p&gt;The first test asserts that both books are in the response context, using &lt;code&gt;assertIn&lt;/code&gt;. It also ensures the length of the &lt;code&gt;response.context[&amp;#39;book_list&amp;#39;]&lt;/code&gt; object.
The second test uses Django&amp;#39;s &lt;code&gt;assertQuerySetEqual&lt;/code&gt;.
This assertion checks the ordering by default, so it does exactly what we need.&lt;/p&gt;
&lt;p&gt;Both tests also check the &lt;code&gt;status_code&lt;/code&gt;, so you know immediately if the URL doesn&amp;#39;t work.&lt;/p&gt;
&lt;h3&gt;Running Our Test with Unittest&lt;/h3&gt;
&lt;p&gt;We can run a test with Django&amp;#39;s &lt;a href=&quot;https://docs.djangoproject.com/en/4.2/ref/django-admin/#test&quot;&gt;test command&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ python manage.py test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;Found 2 test(s).
Creating test database for alias &amp;#39;default&amp;#39;...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.089s

OK
Destroying test database for alias &amp;#39;default&amp;#39;...
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;p&gt;What if we run a failing test? For example, if the order of returned objects doesn&amp;#39;t match the desired result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(venv)$ python manage.py test
Found 2 test(s).
Creating test database for alias &amp;#39;default&amp;#39;...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_book_order (bookstore.test.BookListTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File &amp;quot;/bookstore/test.py&amp;quot;, line 17, in test_book_order
    self.assertEqual(books, [self.second_book, self.first_book])
AssertionError: Lists differ: [&amp;lt;Book: Pride and Prejudice&amp;gt;, &amp;lt;Book: Emma&amp;gt;] != [&amp;lt;Book: Emma&amp;gt;, &amp;lt;Book: Pride and Prejudice&amp;gt;]

First differing element 0:
&amp;lt;Book: Pride and Prejudice&amp;gt;
&amp;lt;Book: Emma&amp;gt;

- [&amp;lt;Book: Pride and Prejudice&amp;gt;, &amp;lt;Book: Emma&amp;gt;]
+ [&amp;lt;Book: Emma&amp;gt;, &amp;lt;Book: Pride and Prejudice&amp;gt;]
----------------------------------------------------------------------
Ran 2 tests in 0.085s

FAILED (failures=1)
Destroying test database for alias &amp;#39;default&amp;#39;...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the output is quite informative — you learn which test failed, in which line, and why.
The test failed because the lists differ; you can even see both lists, so you can quickly determine where the problem lies.&lt;/p&gt;
&lt;p&gt;Another essential thing to notice here is that only the second test failed — figuring out what&amp;#39;s wrong can be more challenging if there&amp;#39;s a single test.&lt;/p&gt;
&lt;h2&gt;Pytest for Python&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pytest.org/en/7.4.x/&quot;&gt;Pytest&lt;/a&gt; is an excellent alternative to unittest. Even though it doesn&amp;#39;t come built-in to &lt;code&gt;Python&lt;/code&gt; itself, it is considered more &lt;em&gt;pythonic&lt;/em&gt; than unittest.
It doesn&amp;#39;t require a &lt;code&gt;TestClass&lt;/code&gt;, has less boilerplate code, and has a plain &lt;code&gt;assert&lt;/code&gt; statement.
Pytest has a rich plugin ecosystem, including a specific Django plugin, &lt;a href=&quot;https://pytest-django.readthedocs.io/&quot;&gt;pytest-django&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The pytest approach is quite different from unittest.&lt;/p&gt;
&lt;p&gt;Among other things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You can write function-based tests without the need for classes.&lt;/li&gt;
&lt;li&gt;It allows you to create fixtures: reusable components for setup and teardown. Fixtures can have different scopes (e.g., &lt;code&gt;module&lt;/code&gt;), allowing you to optimize performance while keeping a test isolated.&lt;/li&gt;
&lt;li&gt;Parametrization of the test function means it can be efficiently run with different arguments.&lt;/li&gt;
&lt;li&gt;It has a single, plain &lt;code&gt;assert&lt;/code&gt; statement.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Pytest and Django&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;pytest-django&lt;/em&gt; serves as an adapter between Pytest and Django.
Testing Django with pytest without pytest-django is technically possible, but not practical nor recommended.
Along with handling Django&amp;#39;s settings, static files, and templates, it brings some &lt;em&gt;Django test&lt;/em&gt; tools to pytest, but also adds its own:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;@pytest.mark.django_db&lt;/code&gt; decorator that enables database access for a specific test.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;client&lt;/code&gt; that passes Django&amp;#39;s &lt;code&gt;Client&lt;/code&gt; in the form of a fixture.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;admin_client&lt;/code&gt; fixture returns an authenticated &lt;code&gt;Client&lt;/code&gt; with admin access.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;settings&lt;/code&gt; fixture that allows you to temporarily override Django settings during a test.&lt;/li&gt;
&lt;li&gt;The same special Django assertions as &lt;code&gt;Django TestCase&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;How Tests Are Built with Pytest&lt;/h3&gt;
&lt;p&gt;Like unittest, pytest also supports test discovery.
As you&amp;#39;ll see below, the out-of-the-box rules are (these rules can be easily changed):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The files must be named &lt;em&gt;test_*.py&lt;/em&gt; or &lt;em&gt;*_test.py&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;There&amp;#39;s no need for test classes, but if you want to use them, the name should have a &lt;em&gt;Test&lt;/em&gt; prefix.&lt;/li&gt;
&lt;li&gt;The function names need to be prefixed with &lt;em&gt;test&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;#39;s see what tests written with pytest look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from datetime import date

import pytest
from django.urls import reverse

from bookstore.models import Author, Book


@pytest.mark.django_db
def test_book_list_returns_all_books(client):
    author = Author.objects.create(first_name=&amp;#39;Jane&amp;#39;, last_name=&amp;#39;Austen&amp;#39;)
    second_book = Book.objects.create(title=&amp;#39;Emma&amp;#39;, author=author, date_published=date(1815, 1, 1))
    first_book = Book.objects.create(title=&amp;#39;Pride and Prejudice&amp;#39;, author=author, date_published=date(1813, 1, 1))

    response = client.get(reverse(&amp;#39;book_list&amp;#39;))
    books = list(response.context[&amp;#39;book_list&amp;#39;])

    assert response.status_code == 200
    assert first_book in books
    assert second_book in books
    assert len(books) == 2


@pytest.mark.django_db
def test_book_list_ordered_by_date_published(client):
    author = Author.objects.create(first_name=&amp;#39;Jane&amp;#39;, last_name=&amp;#39;Austen&amp;#39;)
    second_book = Book.objects.create(title=&amp;#39;Emma&amp;#39;, author=author, date_published=date(1815, 1, 1))
    first_book = Book.objects.create(title=&amp;#39;Pride and Prejudice&amp;#39;, author=author, date_published=date(1813, 1, 1))

    response = client.get(reverse(&amp;#39;book_list&amp;#39;))
    books = list(response.context[&amp;#39;book_list&amp;#39;])

    assert response.status_code == 200
    assert books == [first_book, second_book]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What&amp;#39;s Happening Here?&lt;/h3&gt;
&lt;p&gt;These two tests assess the same functionality as the previous two unittest tests — the first test ensures all the books in the database are returned, and the second ensures the books are ordered by publication date.
How do they differ?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;They&amp;#39;re entirely standalone — no class or joint data preparation is needed. You could put them in two different files, and nothing would change.&lt;/li&gt;
&lt;li&gt;Since they need database access, they require a &lt;code&gt;@pytest.mark.django_db&lt;/code&gt; decorator that comes from pytest-django. If there&amp;#39;s no DB access required, skip the decorator.&lt;/li&gt;
&lt;li&gt;Django&amp;#39;s &lt;code&gt;client&lt;/code&gt; needs to be passed as a fixture since you don&amp;#39;t automatically have access to it. Again, this comes from pytest-django.&lt;/li&gt;
&lt;li&gt;Instead of different assert statements, a plain &lt;code&gt;assert&lt;/code&gt; is used.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The lines that execute the request and that turn the &lt;code&gt;book_list&lt;/code&gt; in the response are precisely the same as in unittest.&lt;/p&gt;
&lt;h3&gt;Fixtures in Pytest&lt;/h3&gt;
&lt;p&gt;In pytest, there&amp;#39;s no &lt;code&gt;setUpTestData&lt;/code&gt;. But if you find yourself repeating the same data preparation code over and over again, you can use a &lt;a href=&quot;https://docs.pytest.org/en/6.2.x/fixture.html&quot;&gt;fixture&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The fixture in our example would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# fixture creation
import pytest

@pytest.fixture
def two_book_objects_same_author():
    author = Author.objects.create(first_name=&amp;#39;Jane&amp;#39;, last_name=&amp;#39;Austen&amp;#39;)
    second_book = Book.objects.create(title=&amp;#39;Emma&amp;#39;, author=author, date_published=date(1815, 1, 1))
    first_book = Book.objects.create(title=&amp;#39;Pride and Prejudice&amp;#39;, author=author, date_published=date(1813, 1, 1))

    return [first_book, second_book]

# fixture usage
@pytest.mark.django_db
def test_book_list_ordered_by_date_published_with_fixture(client, two_book_objects_same_author):
    response = client.get(reverse(&amp;#39;book_list&amp;#39;))
    books = list(response.context[&amp;#39;book_list&amp;#39;])

    assert response.status_code == 200
    assert books == two_book_objects_same_author
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You mark a fixture with the &lt;code&gt;@pytest.fixture&lt;/code&gt; decorator. You then need to pass it as an argument for a function to use.
You can use fixtures to avoid repetitive code in the preparation step or to improve readability, but don&amp;#39;t overdo it.&lt;/p&gt;
&lt;h3&gt;Running a Test with Pytest for Django&lt;/h3&gt;
&lt;p&gt;For pytest to work correctly, you need to tell django-pytest where Django&amp;#39;s settings can be found.
While this can be done in the terminal (&lt;code&gt;pytest --ds=bookstore.settings&lt;/code&gt;), a better option is to use &lt;a href=&quot;https://pytest-django.readthedocs.io/en/latest/configuring_django.html#pytest-ini-settings&quot;&gt;pytest.ini&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At the same time, you can use &lt;em&gt;pytest.ini&lt;/em&gt; to provide other &lt;a href=&quot;https://docs.pytest.org/en/stable/reference/reference.html#configuration-options&quot;&gt;configuration options&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;pytest.ini&lt;/em&gt; looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[pytest]

;where the django settings are
DJANGO_SETTINGS_MODULE = bookstore.settings

;changing test discovery
python_files = tests.py test_*.py

;output logging records into the console
log_cli = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once &lt;code&gt;DJANGO_SETTINGS_MODULE&lt;/code&gt; is set, you can simply run the tests with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;(venv)$ pytest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&amp;#39;s see the output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;================================================================================================= test session starts ==================================================================================================
platform darwin -- Python 3.10.5, pytest-7.4.3, pluggy-1.3.0
django: settings: bookstore.settings (from option)
rootdir: /bookstore
plugins: django-4.5.2
collected 2 items

bookstore/test_with_pytest.py::test_book_list_returns_all_books_with_pytest PASSED                                                                                                                                   [ 50%]
bookstore/test_with_pytest.py::test_book_list_ordered_by_date_published_with_pytest PASSED                                                                                                                           [100%]

================================================================================================== 2 passed in 0.58s ===================================================================================================
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This output is shown if &lt;code&gt;log_cli&lt;/code&gt; is set to &lt;code&gt;True&lt;/code&gt;. If it&amp;#39;s not explicitly set, it defaults to &lt;code&gt;False&lt;/code&gt;, and the output of each test is replaced by a simple dot.&lt;/p&gt;
&lt;p&gt;And what does the output look like if the test fails?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;================================================================================================= test session starts ==================================================================================================
platform darwin -- Python 3.10.5, pytest-7.4.3, pluggy-1.3.0
django: settings: bookstore.settings (from ini)
rootdir: /bookstore
configfile: pytest.ini
plugins: django-4.5.2
collected 2 items

bookstore/test_with_pytest.py::test_book_list_returns_all_books_with_pytest PASSED                                                                                                                                   [ 50%]
bookstore/test_with_pytest.py::test_book_list_ordered_by_date_published_with_pytest FAILED                                                                                                                           [100%]

======================================================================================================= FAILURES =======================================================================================================
___________________________________________________________________________________________________ test_book_order ____________________________________________________________________________________________________

client = &amp;lt;django.test.client.Client object at 0x1064dbfa0&amp;gt;, books_in_correct_order = [&amp;lt;Book: Emma&amp;gt;, &amp;lt;Book: Pride and Prejudice&amp;gt;]

    @pytest.mark.django_db
    def test_book_order(client, books_in_correct_order):
        response = client.get(reverse(&amp;#39;book_list&amp;#39;))
        books = list(response.context[&amp;#39;book_list&amp;#39;])

&amp;gt;       assert books == books_in_correct_order
E       assert [&amp;lt;Book: Pride... &amp;lt;Book: Emma&amp;gt;] == [&amp;lt;Book: Emma&amp;gt;...nd Prejudice&amp;gt;]
E         At index 0 diff: &amp;lt;Book: Pride and Prejudice&amp;gt; != &amp;lt;Book: Emma&amp;gt;
E         Full diff:
E         - [&amp;lt;Book: Emma&amp;gt;, &amp;lt;Book: Pride and Prejudice&amp;gt;]
E         + [&amp;lt;Book: Pride and Prejudice&amp;gt;, &amp;lt;Book: Emma&amp;gt;]

=============================================================================================== short test summary info ================================================================================================
FAILED bookstore/test_with_pytest.py::test_book_list_ordered_by_date_published_with_pytest - assert [&amp;lt;Book: Pride... &amp;lt;Book: Emma&amp;gt;] == [&amp;lt;Book: Emma&amp;gt;...nd Prejudice&amp;gt;]
============================================================================================= 1 failed, 1 passed in 0.78s ==============================================================================================
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;log_cli&lt;/code&gt; is set to &lt;code&gt;True&lt;/code&gt;, you can easily see which test passed and which failed.
Although there&amp;#39;s a long output on the error, you can often find enough information on why the test failed in the &lt;em&gt;short test summary info&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Unittest vs. Pytest for Python&lt;/h2&gt;
&lt;p&gt;There&amp;#39;s no consensus as to whether unittest or pytest is better. The opinions of developers differ, as you can see
on &lt;a href=&quot;https://stackoverflow.com/questions/44558018/django-test-vs-pytest&quot;&gt;StackOverflow&lt;/a&gt;
or &lt;a href=&quot;https://www.reddit.com/r/django/comments/zcarme/testing_in_django_with_unittest_or_another_package/&quot;&gt;Reddit&lt;/a&gt;.
A lot depends on your own preferences or those of your team.
If most of your team is used to pytest, there is no need to switch, and vice-versa.&lt;/p&gt;
&lt;p&gt;However, if you don&amp;#39;t have your preferred way of testing yet, here&amp;#39;s a quick comparison between the two:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;unittest (with Django test)&lt;/th&gt;
&lt;th&gt;pytest (with pytest-django)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;No additional installation needed&lt;/td&gt;
&lt;td&gt;Installation of pytest and pytest-django required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class-based approach&lt;/td&gt;
&lt;td&gt;Functional approach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple types of assertions&lt;/td&gt;
&lt;td&gt;A plain &lt;code&gt;assert&lt;/code&gt; statement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup/TearDown methods within &lt;code&gt;TestCase&lt;/code&gt; class&lt;/td&gt;
&lt;td&gt;Fixture system with different scopes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parametrization isn&amp;#39;t supported (but it can be done on your own)&lt;/td&gt;
&lt;td&gt;Native support for parametrization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;How to Approach Tests in Django&lt;/h2&gt;
&lt;p&gt;Django encourages rapid development, so you might feel that you don&amp;#39;t actually need tests as
they would just slow you down. However, tests will, in the long run, make your development process faster and less error-prone.
Since Django provides a lot of building blocks that allow you to accomplish much with only a few lines of code, you may end up writing way more tests than actual code.&lt;/p&gt;
&lt;p&gt;One thing that can help you cover most of your code with tests is &lt;em&gt;code coverage&lt;/em&gt;.
&amp;quot;&lt;em&gt;Code coverage&lt;/em&gt;&amp;quot; is a measure that tells you what percentage of your program&amp;#39;s code is executed during a test suite run.
The higher the percentage, the bigger the portion of the code being tested, which usually means fewer unnoticed problems.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://coverage.readthedocs.io&quot;&gt;Coverage.py&lt;/a&gt; is the go-to tool for measuring code coverage of Python programs.
Once installed, you can use it with either &lt;em&gt;unittest&lt;/em&gt; or &lt;em&gt;pytest&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;If using pytest, you can install &lt;a href=&quot;https://pypi.org/project/pytest-cov/&quot;&gt;pytest-cov&lt;/a&gt; — a library that integrates Coverage.py with pytest.
It is widely used, but the &lt;a href=&quot;https://coverage.readthedocs.io/en/7.3.2/#quick-start&quot;&gt;coverage.py official documentation states that it&amp;#39;s mostly unnecessary&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The commands for running coverage are a little different for unittest and pytest:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;For unittest: &lt;code&gt;coverage run manage.py test&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For pytest: &lt;code&gt;coverage run -m pytest&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Those commands will run tests and check the coverage, but you must run a separate command to get the output.&lt;/p&gt;
&lt;p&gt;To see the report in your terminal, you need to run the &lt;code&gt;coverage report&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$(venv) coverage report
Name                                                                             Stmts   Miss  Cover
------------------------------------------------------------------------------------------------------
core/__init__.py                                                         0      0    100%
core/settings.py                                                         34     0    100%
core/urls.py                                                             3      0    100%
bookstore/__init__.py                                                             0      0    100%
bookstore/admin.py                                                                19     1    95%
bookstore/apps.py                                                                 6      0    100%
bookstore/models.py                                                               64     7    89%
bookstore/urls.py                                                                 3      0    100%
bookstore/views.py                                                                34     7    79%
------------------------------------------------------------------------------------------------------
TOTAL                                                                             221     15    93%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Percentages ranging from 75% to 100% are seen as giving &amp;quot;&lt;em&gt;good coverage&lt;/em&gt;&amp;quot;.
Aim for a score higher than 75%, but keep in mind that just because your code is well-covered by tests, doesn&amp;#39;t mean it is any good.&lt;/p&gt;
&lt;h3&gt;A Note on Tests&lt;/h3&gt;
&lt;p&gt;Test coverage is one aspect of a good test suite. However, as long as your coverage is not below 75%, the exact percentage is not that significant.&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t write tests just for the sake of writing them or to improve your coverage percentage.
Not everything in Django needs testing — writing useless tests will slow down your test suite and make refactoring a slow and painful process.&lt;/p&gt;
&lt;p&gt;You should not test Django&amp;#39;s own code — it&amp;#39;s already been tested.
For example, you don&amp;#39;t need to write a test that checks if an object is retrieved with &lt;code&gt;get_object_or_404&lt;/code&gt; — &lt;a href=&quot;https://github.com/django/django/blob/main/tests/get_object_or_404/tests.py&quot;&gt;Django&amp;#39;s testing suite already has that covered&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Also, not every implementation detail needs to be tested.
For example, I rarely check if the correct template is used for a response — if I change the name, the test will fail without providing any real value.&lt;/p&gt;
&lt;h3&gt;How Can AppSignal Help with Testing in Django?&lt;/h3&gt;
&lt;p&gt;The trouble with tests is that they&amp;#39;re written in isolation — when your app is live, it might not behave the same, and the user will probably not behave just as you expected.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s why it&amp;#39;s a good idea to use application monitoring — it can help you track errors and monitor performance.
Most importantly, it can inform you when an error is fatal, breaking your app.&lt;/p&gt;
&lt;p&gt;AppSignal integrates with &lt;a href=&quot;https://www.appsignal.com/python/django-monitoring&quot;&gt;Django&lt;/a&gt; and can help you find the &lt;a href=&quot;https://www.appsignal.com/tour/errors&quot;&gt;errors you miss&lt;/a&gt; when writing tests.&lt;/p&gt;
&lt;p&gt;With AppSignal, you can see how often an error occurs and when. Knowing that can help you decide how urgently you need to fix the error.&lt;/p&gt;
&lt;p&gt;Since you can integrate AppSignal with your Git repository, you can also pinpoint the line in which an error occurs. This simplifies the process of identifying an error&amp;#39;s cause, making it easier to fix.
When an error is well understood, it&amp;#39;s also easier to add tests that prevent it and similar errors in the future.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s an example of an error from the &lt;a href=&quot;https://www.appsignal.com/tour/errors&quot;&gt;Issues list under the Errors dashboard for a Django application&lt;/a&gt; in AppSignal:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2024-01/django-issue-list.png&quot; alt=&quot;Error on a Django dashboard&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In this post, we&amp;#39;ve seen that you have two excellent options for testing in Django: &lt;em&gt;unittest&lt;/em&gt; and &lt;em&gt;pytest&lt;/em&gt;. We introduced both options, highlighting their unique strengths and capabilities. Which one you choose largely hinges on your personal or project-specific preferences.&lt;/p&gt;
&lt;p&gt;I hope you&amp;#39;ve seen how writing tests is essential to having a high-quality product that provides a seamless user experience.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to Deploy a Python Flask app with Heroku</title>
    <link rel="alternate" href="https://blog.appsignal.com/2023/12/06/how-to-deploy-a-python-flask-app-with-heroku.html"/>
    <id>https://blog.appsignal.com/2023/12/06/how-to-deploy-a-python-flask-app-with-heroku.html</id>
    <published>2023-12-06T00:00:00+00:00</published>
    <updated>2023-12-06T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">Let&#039;s build a simple Flask app that is primed and ready to deploy to Heroku.</summary>
    <content type="html">&lt;p&gt;In this tutorial, we will build a simple Flask app that is primed and ready to deploy to Heroku.&lt;/p&gt;
&lt;p&gt;Once the bare bones of the app are built, we will guide you through the setup process on GitHub and Heroku so that you can start making automatic deploys in no time.&lt;/p&gt;
&lt;p&gt;But before we dive straight into the code, why choose Flask and Heroku in the first place?&lt;/p&gt;
&lt;h2&gt;Why Flask for Python?&lt;/h2&gt;
&lt;p&gt;Flask is a super-minimalistic web framework for Python. It provides only the absolute core functionality needed to build web applications in a small and nimble package.&lt;/p&gt;
&lt;p&gt;One major advantage of this approach is that Flask provides web developers with maximum flexibility to design and build their app from the very beginning.&lt;/p&gt;
&lt;p&gt;On the other hand, frameworks such as Django prefer the &amp;quot;batteries-included&amp;quot; philosophy. This means more is taken care of for you out-of-the-box, but there will be more boilerplate code. The structure of your application is more rigid and harder to change later on.&lt;/p&gt;
&lt;p&gt;Also, as the industry moves away from big code &amp;quot;monoliths&amp;quot; built on frameworks like Django, and towards smaller microservice architecture, it has never been a better time to get familiar with Flask.&lt;/p&gt;
&lt;h2&gt;Why Heroku?&lt;/h2&gt;
&lt;p&gt;When Heroku launched in 2007, it was one of the first cloud platforms renowned for its user-friendly interface and ease of use. Using Heroku was a huge time-saver, as it made deploying web applications much easier than configuring all your infrastructure from scratch using AWS. In fact, Heroku itself is built on AWS infrastructure. Another advantage was the fact that you could deploy your hobby app with a full Postgres database totally for free.&lt;/p&gt;
&lt;p&gt;Since then, several other cloud services have sprung up to challenge Heroku&amp;#39;s first-mover advantage in the sector of cloud computing known as Platform-as-a-Service (PaaS).&lt;/p&gt;
&lt;p&gt;But Heroku still retains one significant edge over the newer cloud providers: its extensive library of third-party add-ons.&lt;/p&gt;
&lt;p&gt;If you need functionality for your app like error tracking or performance monitoring (which, incidentally, &lt;a href=&quot;https://www.appsignal.com/python&quot;&gt;AppSignal&lt;/a&gt; provides) you can simply install an add-on for that on Heroku&amp;#39;s interface and get started immediately. For many developers, this speed and convenience might make the difference between shipping or not shipping.&lt;/p&gt;
&lt;h2&gt;Create a Python Flask Demo App for Heroku Deployment&lt;/h2&gt;
&lt;p&gt;The very first thing we need to do is create an empty Flask project. Lets call it &lt;code&gt;flask-heroku-appsignal&lt;/code&gt;. We&amp;#39;ll also create the main entry point file for our project, &lt;code&gt;app.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;mkdir flask_heroku_appsignal
cd flask-heroku-appsignal
touch app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we&amp;#39;ll make our project a git repository by running the git initialize command in the root of the directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By the way, if you want to view the full code for this article at any point, &lt;a href=&quot;https://github.com/daneasterman/flask-heroku-appsignal&quot;&gt;download or clone it here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, we need to install the core dependencies, which include Flask itself, &lt;code&gt;python-dotenv&lt;/code&gt; (to manage environment variables), and &lt;code&gt;gunicorn&lt;/code&gt;, which will act as the &amp;quot;bridge&amp;quot; between Heroku&amp;#39;s web servers and our web application.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;requirements.txt&lt;/code&gt; file in the root of the project and just add the dependency names to it (you don&amp;#39;t need to include the version numbers):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Flask
python-dotenv
gunicorn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything goes smoothly, we should see some output that looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;Collecting Flask
  Using cached Flask-2.2.3-py3-none-any.whl (101 kB)
Collecting python-dotenv
  Using cached python_dotenv-1.0.0-py3-none-any.whl (19 kB)
Collecting Werkzeug&amp;gt;=2.2.2
  Using cached Werkzeug-2.2.3-py3-none-any.whl (233 kB)
Collecting itsdangerous&amp;gt;=2.0
  Using cached itsdangerous-2.1.2-py3-none-any.whl (15 kB)
Collecting click&amp;gt;=8.0
  Using cached click-8.1.3-py3-none-any.whl (96 kB)
Collecting Jinja2&amp;gt;=3.0
  Using cached Jinja2-3.1.2-py3-none-any.whl (133 kB)
Collecting MarkupSafe&amp;gt;=2.0
  Using cached MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl (13 kB)
Installing collected packages: python-dotenv, MarkupSafe, itsdangerous, click, Werkzeug, Jinja2, Flask
Successfully installed Flask-2.2.3 Jinja2-3.1.2 MarkupSafe-2.1.2 Werkzeug-2.2.3 click-8.1.3 itsdangerous-2.1.2 python-dotenv-1.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;Banner lang=&quot;python&quot; /&gt;

&lt;h3&gt;Preparing for Heroku Deployment&lt;/h3&gt;
&lt;p&gt;Now that all our basic project foundations are in place, let’s start building and configuring our Flask app with deployment to Heroku in mind.&lt;/p&gt;
&lt;p&gt;First, let&amp;#39;s copy this super-simple &amp;quot;hello world&amp;quot; code into &lt;code&gt;app.py&lt;/code&gt; to check everything is working as expected so far:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;from flask import Flask
from dotenv import load_dotenv
load_dotenv()

app = Flask(__name__)

@app.route(&amp;quot;/&amp;quot;)
def index():
  return &amp;quot;Hello World!&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then type &lt;code&gt;flask run&lt;/code&gt; in the terminal. We should see the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we go to &lt;code&gt;localhost:5000&lt;/code&gt;, we will see the &lt;code&gt;Hello World!&lt;/code&gt; string printed in the browser.&lt;/p&gt;
&lt;p&gt;At this point, I have a nice time-saving Flask tip to share with you that isn&amp;#39;t mentioned in most Flask tutorials.&lt;/p&gt;
&lt;p&gt;By adding the simple one-liner below in your &lt;code&gt;.env&lt;/code&gt; at the root of the project, you can save lots of time. You won&amp;#39;t have to stop and start your local server every time you want to make changes to your backend code. While this doesn&amp;#39;t sound like a huge thing, these small changes add up to loads of compounded saved time over the long run.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;FLASK_DEBUG=True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As the “debug” name suggests, this setting will provide a nicely formatted error read-out in the browser to make it easier to debug your code.&lt;/p&gt;
&lt;p&gt;Also, ensure that the &lt;code&gt;.env&lt;/code&gt; file is included in your project&amp;#39;s &lt;code&gt;.gitignore&lt;/code&gt; so that it&amp;#39;s not tracked by git. This means it will not be picked up by GitHub and Heroku (as we don’t want the debug mode to be part of our production settings).&lt;/p&gt;
&lt;p&gt;After making this change, the output on our terminal will display &lt;code&gt;Debug mode&lt;/code&gt; as &lt;code&gt;on&lt;/code&gt; with the Debugger PIN:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 133-697-373
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Development and Production Settings In Our Python Flask App&lt;/h3&gt;
&lt;p&gt;Next, we want to make a clear distinction between the development and production settings in our Flask app. This is an important step in preparing for deployment to Heroku. To do this, we will create a &lt;code&gt;config.py&lt;/code&gt; file in the root of the project and add the following code with three classes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;class Config:
 DEBUG = False
 DEVELOPMENT = False
 CSRF_ENABLED = True
 ASSETS_DEBUG = False

class ProductionConfig(Config):
 pass

class DevelopmentConfig(Config):
 DEBUG = True
 DEVELOPMENT = True
 TEMPLATES_AUTO_RELOAD = True
 ASSETS_DEBUG = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Back in &lt;code&gt;app.py&lt;/code&gt;, add the following two lines before the index route function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;...
env_config = os.getenv(&amp;quot;PROD_APP_SETTINGS&amp;quot;, &amp;quot;config.DevelopmentConfig&amp;quot;)
app.config.from_object(env_config)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now the full &lt;code&gt;app.py&lt;/code&gt; will look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import os
from flask import Flask
from dotenv import load_dotenv
load_dotenv()

app = Flask(__name__)

env_config = os.getenv(&amp;quot;PROD_APP_SETTINGS&amp;quot;, &amp;quot;config.DevelopmentConfig&amp;quot;)
app.config.from_object(env_config)

@app.route(&amp;quot;/&amp;quot;)
def index():
    return &amp;quot;Hello World!&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Essentially, the code above says that if we find a key called &lt;code&gt;PROD_APP_SETTINGS&lt;/code&gt; in our environment, the config for our app should be set to &lt;code&gt;config.ProductionConfig&lt;/code&gt;. Otherwise, we can safely assume we are in development mode and set it to &lt;code&gt;config.DevelopmentConfig&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now we are ready to set up our app in Heroku&amp;#39;s web interface.&lt;/p&gt;
&lt;h2&gt;GitHub Setup&lt;/h2&gt;
&lt;p&gt;Before setting up on Heroku, we first need to create a repository on GitHub.&lt;/p&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://github.com/&quot;&gt;github.com&lt;/a&gt; and create a new account (if you don&amp;#39;t have one already).&lt;/p&gt;
&lt;p&gt;We can call our GitHub repository the same name as our local Flask project:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2023-12/github-repo.png&quot; alt=&quot;Create GitHub Repository Screenshot&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Now we simply need to follow GitHub&amp;#39;s instructions and push the code from our existing local repository to the remote one.&lt;/p&gt;
&lt;p&gt;To double-check that the local and remote repositories have been linked correctly, run this command in the project directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git remote -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We should get something very similar to the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;origin  https://github.com/daneasterman/flask-heroku-appsignal.git (fetch)
origin  https://github.com/daneasterman/flask-heroku-appsignal.git (push)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the GitHub steps complete, we can start getting set up on Heroku by first creating a new app.&lt;/p&gt;
&lt;h2&gt;Heroku Deployment&lt;/h2&gt;
&lt;p&gt;We need to make sure that we have Heroku&amp;#39;s special &lt;code&gt;Procfile&lt;/code&gt; at the root of our project. The Procfile is a configuration text file specifying the various process types that will run when your app starts up. Without this, your app will not run on Heroku.&lt;/p&gt;
&lt;p&gt;Copy the line below and add it to your Procfile:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;web: gunicorn app:app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, remember the &lt;code&gt;config.py&lt;/code&gt; we created in the previous section? We need to update our configuration in Heroku so that the app runs on our production settings, with &lt;code&gt;DEBUG&lt;/code&gt; set to &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First, click on the Setting tab on your Heroku dashboard. Then click the &lt;strong&gt;Reveal Config Vars&lt;/strong&gt; button. Add the variable &lt;code&gt;PROD_APP_SETTINGS&lt;/code&gt; and the value &lt;code&gt;config.ProductionConfig&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, we need to connect our GitHub repository to Heroku. Click on the &lt;strong&gt;&amp;quot;Connect to GitHub&amp;quot;&lt;/strong&gt; button. Then in the section that appears underneath, search for our &lt;code&gt;flask-heroku-appsignal&lt;/code&gt; repository, and finally click on the second &lt;strong&gt;&amp;quot;Connect&amp;quot;&lt;/strong&gt; button below.&lt;/p&gt;
&lt;p&gt;Then click the &lt;strong&gt;&amp;quot;Enable Automatic Deploys&amp;quot;&lt;/strong&gt; button. This means that every time you push your code to the main branch on GitHub, it will trigger an automatic deployment to Heroku.&lt;/p&gt;
&lt;p&gt;Unfortunately, Heroku doesn&amp;#39;t provide a free tier for demo apps anymore. So to complete the proof-of-concept for our &lt;code&gt;flask-heroku-appsignal&lt;/code&gt; app, we need to purchase a dyno-type subscription.&lt;/p&gt;
&lt;p&gt;This is where Heroku is a little cheeky and automatically puts you in the slightly more expensive &lt;strong&gt;Basic Plan&lt;/strong&gt;. To change this, click on the &lt;strong&gt;Resources&lt;/strong&gt; tab and then click: &lt;strong&gt;Change Dyno Type&lt;/strong&gt;, which will bring up a modal screen. Finally select the &lt;strong&gt;Eco&lt;/strong&gt; dyno, which will be absolutely fine for our purposes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2023-12/dyno-types.png&quot; alt=&quot;Heroku Dyno Types Screen&quot;/&gt;&lt;/p&gt;
&lt;p&gt;And we&amp;#39;re done!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;That concludes this introduction to deploying your Flask app to Heroku. Once you build out your web app with more functionality, you can think about installing the &lt;a href=&quot;https://devcenter.heroku.com/articles/appsignal&quot;&gt;AppSignal add-on for Heroku&lt;/a&gt;, which now supports Python and Flask apps.&lt;/p&gt;
&lt;p&gt;With this add-on, you can track errors, monitor performance, get access to advanced metric dashboards, and more. Happy building!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S. If you&amp;#39;d like to read Python posts as soon as they get off the press, &lt;a href=&quot;/python-wizardry&quot;&gt;subscribe to our Python Wizardry newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>AppSignal Monitoring Available for Python Applications</title>
    <link rel="alternate" href="https://blog.appsignal.com/2023/10/31/appsignal-monitoring-available-for-python-applications.html"/>
    <id>https://blog.appsignal.com/2023/10/31/appsignal-monitoring-available-for-python-applications.html</id>
    <published>2023-10-31T00:00:00+00:00</published>
    <updated>2023-10-31T00:00:00+00:00</updated>
    <author>Roy Tomeij</author>
    <summary type="html">AppSignal is proud to help Python developers level up their application monitoring.</summary>
    <content type="html">&lt;p&gt;We&amp;#39;re happy to announce that &lt;a href=&quot;https://www.appsignal.com/python&quot;&gt;AppSignal now offers monitoring tools for Python projects&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;AppSignal helps you get the most out of your Python application&amp;#39;s monitoring metrics, with additional support for multiple Python frameworks and packages such as Django and Celery.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll walk you through some of our core features to show you how to power up your Python application with AppSignal.&lt;/p&gt;
&lt;h2&gt;Monitoring Support for Popular Python Libraries&lt;/h2&gt;
&lt;p&gt;AppSignal already supports various popular Python libraries, which will only expand with time! We currently offer support for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Celery&lt;/li&gt;
&lt;li&gt;Django&lt;/li&gt;
&lt;li&gt;FastAPI&lt;/li&gt;
&lt;li&gt;Flask&lt;/li&gt;
&lt;li&gt;Jinja2&lt;/li&gt;
&lt;li&gt;Psycopg2&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Requests&lt;/li&gt;
&lt;li&gt;Starlette&lt;/li&gt;
&lt;li&gt;WSGI/ASGI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Is there a Python package we don&amp;#39;t support? &lt;a href=&quot;mailto:support@appsignal.com&quot;&gt;Reach out to us&lt;/a&gt;, and we&amp;#39;ll investigate it further with you.&lt;/p&gt;
&lt;h2&gt;Error Monitoring&lt;/h2&gt;
&lt;p&gt;AppSignal is a tool that tells you when, why, and where errors occur in your application.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2023-10/python-error-detail.png&quot; alt=&quot;Screenshot of Python error&quot;/&gt;&lt;/p&gt;
&lt;p&gt;AppSignal can be configured to track errors per deployment, and tools like Time Detective give you a helicopter view of your application. When an error occurs, you can connect the dots and spend more time solving problems rather than speculating over them.&lt;/p&gt;
&lt;Video vimeoUrl=&quot;https://player.vimeo.com/video/809749481&quot; /&gt;

&lt;p&gt;AppSignal gives you control of when and where you&amp;#39;re notified about errors in your application, whether that&amp;#39;s on Slack or Discord; you can even connect AppSignal to popular collaboration tools like GitHub and Jira and leave crucial contextual information about an error in the Logbook.&lt;/p&gt;
&lt;h2&gt;Performance Monitoring&lt;/h2&gt;
&lt;p&gt;AppSignal monitors your application and tells you when its performance is suboptimal. Spot and fix performance bottlenecks so you can scale your app with confidence.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2023-10/python-performance-monitoring.png&quot; alt=&quot;Screenshot of Python error&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Our intuitive UI helps you see where and when performance problems arise, with all the critical insights you need at your fingertips to choose the best resolution.&lt;/p&gt;
&lt;h2&gt;Anomaly Detection&lt;/h2&gt;
&lt;p&gt;Anomaly detection lets you define the boundaries of what you consider to be good performance in your application and notifies you when and where metrics go over their limits.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2023-10/python-anomalies.png&quot; alt=&quot;Screenshot of Python anomaly detection&quot;/&gt;&lt;/p&gt;
&lt;p&gt;Create custom triggers to alert you when performance dips, background job count increases by 20%, or memory suddenly increases.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2023-10/python-anomalies-triggers.png&quot; alt=&quot;Screenshot of Python triggers&quot;/&gt;&lt;/p&gt;
&lt;p&gt;AppSignal gives you the data-driven confidence to act proactively instead of urgently reacting to issues in your application.&lt;/p&gt;
&lt;h2&gt;Host Monitoring&lt;/h2&gt;
&lt;p&gt;AppSignal also monitors your Python application&amp;#39;s hosts, giving you at-a-glance access to CPU, disk, network, and memory metrics. This allows you to understand how your Python application is performing beyond just looking at its code. Effectively manage and monitor your hosts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2023-10/python-hosts.png&quot; alt=&quot;Screenshot of host metrics&quot;/&gt;&lt;/p&gt;
&lt;h2&gt;Python Dashboards&lt;/h2&gt;
&lt;p&gt;AppSignal&amp;#39;s dashboards give you instant visual insights into your application&amp;#39;s metrics, allowing you to track and trace performance metrics quickly.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/2023-10/python-metric-dashboards.png&quot; alt=&quot;Screenshot of metric dashboard&quot;/&gt;&lt;/p&gt;
&lt;p&gt;If you see something you want to investigate further, click on that point on the chart, and you&amp;#39;ll view the state of your application at that specific point in time. Add custom markers to help you and your team better understand how your application performs.&lt;/p&gt;
&lt;Video vimeoUrl=&quot;https://player.vimeo.com/video/852943845&quot; /&gt;

&lt;h2&gt;More Than Just Monitoring&lt;/h2&gt;
&lt;p&gt;AppSignal can ingest and store logs from popular platforms such as AWS Kinesis, Heroku, and Netlify or via our dedicated logging API endpoint. Gain deep insights into your code&amp;#39;s performance by combining your APM and log management.&lt;/p&gt;
&lt;p&gt;Our intuitive UI takes you from an error message to a log line in just a few clicks and lets you query, filter, and share logs effortlessly.&lt;/p&gt;
&lt;Video vimeoUrl=&quot;https://player.vimeo.com/video/783666526&quot; /&gt;

&lt;h2&gt;Effortless Setup, Immediate Impact&lt;/h2&gt;
&lt;p&gt;You can have your Python application push metrics to AppSignal in less time than it takes to drink a coffee.&lt;/p&gt;
&lt;p&gt;Sign up for an AppSignal account and follow our &lt;a href=&quot;https://appsignal.com/redirect-to/organization?to=sites/new/python&quot;&gt;installation wizard instructions&lt;/a&gt;. The installation wizard will walk you through all the steps needed to send metrics from your Python application to AppSignal!&lt;/p&gt;
&lt;p&gt;Our &lt;a href=&quot;https://docs.appsignal.com/python&quot;&gt;Python documentation&lt;/a&gt; will also take you through all the steps required to get the metrics you need, including how you can install AppSignal manually.&lt;/p&gt;
&lt;h2&gt;AppSignal: Your Python APM Tool&lt;/h2&gt;
&lt;p&gt;AppSignal is built by developers for developers to give you the visibility and insights needed to manage and scale your Python application effectively. AppSignal offers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 360-degree view of your application data.&lt;/li&gt;
&lt;li&gt;An intuitive interface that&amp;#39;s easy to navigate.&lt;/li&gt;
&lt;li&gt;A clean approach to connecting errors, logs, and performance incidents.&lt;/li&gt;
&lt;li&gt;The ability to monitor your Node.js, front-end JavaScript, Ruby, Elixir, and Python applications in one place.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;#39;re a new trial user, &lt;a href=&quot;mailto:support@appsignal.com&quot;&gt;reach out to us&lt;/a&gt; once you&amp;#39;ve got your Python application set up with AppSignal, and we&amp;#39;ll send a package of stroopwafels to you 🍪!&lt;/p&gt;
</content>
  </entry>
  </feed>
