<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Daily Python Projects]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!gfXP!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e0e0918-4155-47ff-ba5d-c061609cbc83_1000x1000.png</url><title>Daily Python Projects</title><link>https://dailypythonprojects.substack.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 11 Jun 2026 14:06:15 GMT</lastBuildDate><atom:link href="https://dailypythonprojects.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Ardit Sulce]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[dailypythonprojects@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[dailypythonprojects@substack.com]]></itunes:email><itunes:name><![CDATA[Ardit Sulce]]></itunes:name></itunes:owner><itunes:author><![CDATA[Ardit Sulce]]></itunes:author><googleplay:owner><![CDATA[dailypythonprojects@substack.com]]></googleplay:owner><googleplay:email><![CDATA[dailypythonprojects@substack.com]]></googleplay:email><googleplay:author><![CDATA[Ardit Sulce]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Web Scraping with BeautifulSoup: Day 2 - Multi-Page Scraping with Pagination]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/web-scraping-with-beautifulsoup-day-3e6</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/web-scraping-with-beautifulsoup-day-3e6</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Thu, 11 Jun 2026 13:01:54 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c1c8b461-9457-41a2-a6f6-c98f5fac084a_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>Web Scraping with BeautifulSoup</strong> suite that extracts a complete product catalog from a real website, scales up to thousands of items across many pages, and turns the scraped data into a searchable mini-database.</p><ul><li><p><strong>Day 1:</strong> Scrape a Single Page</p></li><li><p><strong>Day 2:</strong> Multi-Page Scraping with Pagination <strong>(Today)</strong></p></li><li><p><strong>Day 3:</strong> Search &amp; Analyze the Scraped Data</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-21">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>Yesterday we scraped 20 books from one page. Today we scrape <strong>1,000 books from 50 pages</strong> &#8212; the full catalog of books.toscrape.com. Then we go deeper: for each book, we also visit its detail page to pull richer information you can&#8217;t get from the listing &#8212; the exact UPC, real stock count, full description, and category.</p><p>This is what a real scraper looks like: it follows links, scales across pages, and handles failures politely. By the end of today, you&#8217;ll have a single CSV holding every book on the site, ready for Day 3&#8217;s search tool.</p><h2>Project Task</h2><p>Build a multi-page book scraper that:</p><ul><li><p>Scrapes every book across all 50 catalog pages</p></li><li><p>Detects the &#8220;next page&#8221; link to follow pagination automatically</p></li><li><p>Visits each book&#8217;s detail page to pull richer fields (UPC, stock count, description, category)</p></li><li><p>Adds a polite delay between requests</p></li><li><p>Shows clear progress (page X of Y, book X of Y)</p></li><li><p>Continues running if one page or detail page fails</p></li><li><p>Saves everything to a single CSV</p></li><li><p>Reports a final summary with counts and totals</p></li></ul><p>This project gives you hands-on practice with pagination, two-level scraping (listing &#8594; detail page), polite rate limiting, robust error handling, progress reporting, and combining data from multiple HTTP requests &#8212; the techniques behind every production-grade scraper.</p><h2>Expected Output</h2><p><strong>Running the full-catalog scraper:</strong></p><pre><code><code>python scrape_all_books.py
</code></code></pre><p><strong>Console Output:</strong></p><p>The script will run and visit different pages (50 in total) of the books.toscrape.com website and print out info for each book in the terminal:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ayU9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ayU9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png 424w, https://substackcdn.com/image/fetch/$s_!ayU9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png 848w, https://substackcdn.com/image/fetch/$s_!ayU9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!ayU9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ayU9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1593361,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/201590935?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ayU9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png 424w, https://substackcdn.com/image/fetch/$s_!ayU9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png 848w, https://substackcdn.com/image/fetch/$s_!ayU9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!ayU9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0fae04-3ce2-4052-89c0-5eb9bfcc9ae3_2580x1720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong>Generated </strong><code>all_books.csv</code><strong>:</strong></p><p><strong>In addition to being printed in the terminal, the data will also be saved in a CSV file. Here is a snapshot of the file:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_FR2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_FR2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png 424w, https://substackcdn.com/image/fetch/$s_!_FR2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png 848w, https://substackcdn.com/image/fetch/$s_!_FR2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png 1272w, https://substackcdn.com/image/fetch/$s_!_FR2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_FR2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png" width="1456" height="148" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cb610141-7278-491a-89cc-6804278054d8_1926x196.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:148,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:108995,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/201590935?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_FR2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png 424w, https://substackcdn.com/image/fetch/$s_!_FR2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png 848w, https://substackcdn.com/image/fetch/$s_!_FR2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png 1272w, https://substackcdn.com/image/fetch/$s_!_FR2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb610141-7278-491a-89cc-6804278054d8_1926x196.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>We will have 1,000 rows, 9 columns, every book on the site. Ready for tomorrow&#8217;s search tool.</p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install requests beautifulsoup4
</code></code></pre><p>(Same as Day 1 &#8212; no new dependencies.)</p><p><strong>Run it:</strong></p><pre><code><code>python scrape_all_books.py</code></code></pre><p>The full run takes <strong>roughly 5-10 minutes</strong> depending on your internet &#8212; that&#8217;s 50 listing pages + 1,000 detail pages = <strong>1,050 requests</strong> with a 0.2 second delay between each. The script prints progress so you&#8217;ll know it&#8217;s working.</p><blockquote><p><strong>Tip:</strong> if you just want to see it work without waiting, set <code>MAX_PAGES = 3</code> near the top of the script for a quick three-page test.</p></blockquote><h2>Understanding Pagination</h2><p>The first scaling problem is <em>finding all the pages</em>. We could hardcode <code>page-1.html</code> through <code>page-50.html</code>, but that breaks the moment the site adds page 51. The robust way is to <strong>let the page tell us where to go next</strong>.</p><p>Look at the bottom of any catalog page and you&#8217;ll find:</p><pre><code><code>&lt;li class="next"&gt;
  &lt;a href="page-2.html"&gt;next&lt;/a&gt;
&lt;/li&gt;
</code></code></pre><p>So our strategy is: scrape the current page, look for <code>&lt;li class="next"&gt;</code>, follow it. When that link doesn&#8217;t exist, we&#8217;re done.</p><pre><code><code>def find_next_url(soup, current_url):
    """Return the absolute URL of the next page, or None if this is the last."""
    next_li = soup.find("li", class_="next")
    if next_li is None:
        return None  # we've reached the end of the catalog
    next_href = next_li.find("a")["href"]
    return urljoin(current_url, next_href)
</code></code></pre><p>That <code>urljoin(current_url, ...)</code> is doing real work: pages 2 through 50 use relative links like <code>page-3.html</code>, and <code>urljoin</code> resolves each one <em>against the current page&#8217;s URL</em>. The result is always a correct absolute URL.</p><h2>Understanding the Main Pagination Loop</h2><p>With <code>find_next_url</code> in hand, the main scrape becomes a small <code>while</code> loop:</p><pre><code><code>url = BASE_URL
page_number = 0
all_books = []

while url:
    page_number += 1
    print(f"Page {page_number}:  {url}")

    html = fetch_page(url)
    if html is None:
        break  # can't fetch this page, stop the run

    soup = BeautifulSoup(html, "html.parser")
    books = scrape_listing_page(soup)
    all_books.extend(books)

    url = find_next_url(soup, url)   # None when we've reached the end
    time.sleep(REQUEST_DELAY)
</code></code></pre><p>The whole loop reads in plain English: <em>fetch this page, extract its books, find the next URL, sleep, repeat</em>. When <code>find_next_url</code> returns <code>None</code>, the loop ends naturally. No page counting, no hardcoded limit, no fragility.</p><h2>Understanding Polite Scraping</h2><p>Hitting a server with 1,050 requests as fast as Python can send them is rude &#8212; and a great way to get rate-limited or banned. The fix is a small delay between requests:</p><pre><code><code>import time

REQUEST_DELAY = 0.2   # seconds between requests

# ...inside the loop, after each request:
time.sleep(REQUEST_DELAY)
</code></code></pre><p>0.2 seconds is invisible to you but a <em>huge</em> break for the server &#8212; five requests per second is gentle for any well-running site. The fact that books.toscrape.com explicitly invites scraping doesn&#8217;t change the principle: <strong>every scraper you write should pause between requests</strong>. The habit matters more than this particular site&#8217;s tolerance.</p><h2>Understanding Two-Level Scraping</h2><p>A listing page tells you a book exists. The <strong>detail page</strong> tells you everything <em>about</em> it &#8212; the UPC, the precise stock count, the full description, the category. To get those, we visit each book&#8217;s detail URL.</p><p>The pattern is two-level: scrape the listing, then for each book in that listing, fetch and scrape its detail page.</p><pre><code><code>def enrich_with_details(book):
    """Fetch the book's detail page and add the extra fields to its dict."""
    html = fetch_page(book["url"])
    if html is None:
        # detail fetch failed; keep the listing data, add empty extras
        book.update(stock_count=None, category=None, upc=None, description=None)
        return book

    soup = BeautifulSoup(html, "html.parser")
    book["upc"] = extract_upc(soup)
    book["stock_count"] = extract_stock_count(soup)
    book["category"] = extract_category(soup)
    book["description"] = extract_description(soup)
    return book
</code></code></pre><p>The function takes a book dict (from the listing scrape) and returns the <em>same dict</em> with the extra fields added. This composes cleanly &#8212; listing data and detail data end up in one place.</p><h2></h2><h2>Understanding find_next_sibling</h2><p><code>find_next_sibling("p")</code> is worth a closer look &#8212; it&#8217;s the move for &#8220;find me the <em>next paragraph after this element</em>.&#8221; That&#8217;s how the description is structured on the site: a header div followed by the description paragraph at the same level.</p><pre><code><code>header = soup.find("div", id="product_description")
description_p = header.find_next_sibling("p")
</code></code></pre><p>This pattern shows up constantly in scraping when content is logically <em>related</em> but lives in <em>separate tags</em>. Sibling navigation lets you say &#8220;the thing right after this one,&#8221; without having to know its position in the document. You&#8217;ll reach for it again and again.</p><h2></h2><h2>Understanding Saving the Combined Data</h2><p>The output schema is the union of listing data and detail data &#8212; nine columns total:</p><pre><code><code>FIELDNAMES = [
    "title", "price", "rating", "availability",
    "stock_count", "category", "upc", "description", "url",
]

with open("all_books.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
    writer.writeheader()
    writer.writerows(all_books)
</code></code></pre><p>Because every book is a dict with the same keys, <code>DictWriter</code> aligns the columns correctly even when a few rows have empty <code>description</code> fields. The result is a single, clean CSV &#8212; 1,001 lines including the header &#8212; that becomes the input for Day 3.</p><h2>Coming Tomorrow</h2><p>Tomorrow we put the dataset to work. The <strong>Search &amp; Analyze tool</strong> takes <code>all_books.csv</code> and turns it into a query interface: search by title or keyword, filter by category, price range, and rating, and generate summary statistics &#8212; average prices per category, distribution of ratings, top-N by any field. Your scraped data becomes a small but real, useful database.</p><h2>View Code Evolution</h2><p>Compare today&#8217;s full-catalog scraper with yesterday&#8217;s single-page version and see how a small pagination loop and a detail-page step scale 20 books into 1,000 &#8212; without rewriting the extraction logic.</p><p></p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/web-scraping-with-beautifulsoup-day-3e6">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Web Scraping with BeautifulSoup: Day 1 - Scrape a Single Page]]></title><description><![CDATA[Learn how to fetch a web page, parse it with BeautifulSoup, and extract structured data from a list of repeated items.]]></description><link>https://dailypythonprojects.substack.com/p/web-scraping-with-beautifulsoup-day</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/web-scraping-with-beautifulsoup-day</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Tue, 09 Jun 2026 15:48:37 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ef42ca5c-aacf-40fc-b5ee-659e8fa1729c_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>Web Scraping with BeautifulSoup</strong> suite that extracts a complete product catalog from a real website, scales up to thousands of items across many pages, and turns the scraped data into a searchable mini-database.</p><p><strong>Why build this?</strong> Because web scraping is one of the most genuinely useful Python skills. Data that&#8217;s locked inside a website becomes <em>yours</em> &#8212; to analyze, search, compare, or feed into other tools. Every analyst, data scientist, and automation engineer needs this skill.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong>What you&#8217;ll learn:</strong> This series teaches you HTTP requests, HTML parsing with BeautifulSoup, CSS selectors, handling lists of repeated elements, pagination, polite scraping with delays, structured data extraction, and querying the result.</p><p><strong>Why this matters:</strong> By Day 3, you&#8217;ll have scraped a complete catalog of 1,000 books and built a tool to search and analyze them. That&#8217;s a real, portfolio-worthy project &#8212; and the techniques transfer directly to scraping any catalog-style site.</p><ul><li><p><strong>Day 1:</strong> Scrape a Single Page <strong>(Today)</strong></p></li><li><p><strong>Day 2:</strong> Multi-Page Scraping with Pagination</p></li><li><p><strong>Day 3:</strong> Search &amp; Analyze the Scraped Data</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-21">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>Today you learn the <em>fundamentals</em>: fetching a web page, parsing it with BeautifulSoup, and extracting structured data from a list of repeated items.</p><p>The target is <strong>books.toscrape.com</strong> &#8212; a real, public site explicitly built for scraping practice. It&#8217;s the official sandbox of the Python scraping community. Today&#8217;s mission: scrape every book on the homepage (20 books), capture each one&#8217;s title, price, rating, and availability, and save the result as a clean CSV.</p><p>By the end, you&#8217;ll understand how every catalog-style scraper works &#8212; because the pattern is the same whether you&#8217;re scraping books, products, jobs, real estate, or any other list of repeated items.</p><h2>Project Task</h2><p>Build a single-page book scraper that:</p><ul><li><p>Fetches the books.toscrape.com homepage with <code>requests</code></p></li><li><p>Sets a polite User-Agent header to identify your script</p></li><li><p>Parses the HTML with BeautifulSoup</p></li><li><p>Finds every book card on the page</p></li><li><p>For each book, extracts:</p><ul><li><p>Full title (from the link&#8217;s <code>title</code> attribute, not the truncated text)</p></li><li><p>Price as a real number (no <code>&#163;</code> symbol)</p></li><li><p>Star rating as an integer (1&#8211;5)</p></li><li><p>Stock availability (in stock / out of stock)</p></li><li><p>Link to the book&#8217;s detail page</p></li></ul></li><li><p>Reports progress as it works</p></li><li><p>Saves all books to a CSV with proper column types</p></li></ul><p>This project gives you hands-on practice with <code>requests</code>, BeautifulSoup, CSS class selection, attribute access, text cleanup, and writing structured data to CSV &#8212; the core of every web scraper.</p><h2>Expected Output</h2><p><strong>Running the scraper:</strong></p><pre><code><code>python scrape_books.py</code></code></pre><p><strong>Console Output:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4xGv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4xGv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png 424w, https://substackcdn.com/image/fetch/$s_!4xGv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png 848w, https://substackcdn.com/image/fetch/$s_!4xGv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png 1272w, https://substackcdn.com/image/fetch/$s_!4xGv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4xGv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png" width="1372" height="1862" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1862,&quot;width&quot;:1372,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:359183,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/201318939?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4xGv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png 424w, https://substackcdn.com/image/fetch/$s_!4xGv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png 848w, https://substackcdn.com/image/fetch/$s_!4xGv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png 1272w, https://substackcdn.com/image/fetch/$s_!4xGv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee95baf1-44eb-463b-b64e-d46c90378172_1372x1862.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong>Generated </strong><code>books.csv</code><strong>:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5Evl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5Evl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png 424w, https://substackcdn.com/image/fetch/$s_!5Evl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png 848w, https://substackcdn.com/image/fetch/$s_!5Evl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png 1272w, https://substackcdn.com/image/fetch/$s_!5Evl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5Evl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:683680,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/201318939?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5Evl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png 424w, https://substackcdn.com/image/fetch/$s_!5Evl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png 848w, https://substackcdn.com/image/fetch/$s_!5Evl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png 1272w, https://substackcdn.com/image/fetch/$s_!5Evl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd0d564a-cd9a-4bb7-9b87-06b244c84dd1_1884x1256.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Real prices as numbers, ratings as integers, links absolute &#8212; ready for analysis without further cleanup.</p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install requests beautifulsoup4
</code></code></pre><p>That&#8217;s it &#8212; two libraries. <code>requests</code> fetches the page, <code>beautifulsoup4</code> parses it.</p><p><strong>Run it:</strong></p><pre><code><code>python scrape_books.py
</code></code></pre><p>The script reads from the live site and writes <code>books.csv</code> next to itself.</p><h2>Understanding the Target Site</h2><p>Before writing any code, <strong>always look at the page in your browser</strong>. Open https://books.toscrape.com/ and right-click any book &#8594; &#8220;Inspect Element.&#8221;</p><p>You&#8217;ll see that every book on the page is wrapped in an <code>&lt;article&gt;</code> tag with the class <code>product_pod</code>:</p><pre><code><code>&lt;article class="product_pod"&gt;
  &lt;div class="image_container"&gt;
    &lt;a href="catalogue/a-light-in-the-attic_1000/index.html"&gt;
      &lt;img src="..." alt="A Light in the Attic"&gt;
    &lt;/a&gt;
  &lt;/div&gt;
  &lt;p class="star-rating Three"&gt;&lt;/p&gt;
  &lt;h3&gt;
    &lt;a href="catalogue/a-light-in-the-attic_1000/index.html"
       title="A Light in the Attic"&gt;A Light in the ...&lt;/a&gt;
  &lt;/h3&gt;
  &lt;div class="product_price"&gt;
    &lt;p class="price_color"&gt;&#163;51.77&lt;/p&gt;
    &lt;p class="instock availability"&gt; In stock &lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;
</code></code></pre><p>This structure is the <em>whole game</em>. Every scraper boils down to: find the repeated container, then pluck specific fields out of each one. The HTML inspector tells you the class names and tags to target &#8212; that&#8217;s how you know what to put in your code.</p><h2>Understanding requests and Headers</h2><p>Scraping starts with downloading the page. <code>requests.get()</code> does the heavy lifting:</p><pre><code><code>import requests

url = "https://books.toscrape.com/"
headers = {"User-Agent": "Mozilla/5.0 (Python Scraper)"}

response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()      # blow up clearly if the page didn't load

html = response.text
</code></code></pre><p>Three habits worth forming from day one:</p><ul><li><p><strong>Always set a User-Agent.</strong> Many sites block the default <code>python-requests/...</code> UA. A real-looking string makes your scraper a polite, well-behaved visitor.</p></li><li><p><strong>Always set a timeout.</strong> Without one, a slow or unresponsive server can hang your script forever. Ten seconds is sensible for most pages.</p></li><li><p><strong>Call </strong><code>raise_for_status()</code><strong>.</strong> It throws an exception on 4xx/5xx responses, so you catch bad pages <em>immediately</em> instead of trying to parse an error page.</p></li></ul><h2>Understanding BeautifulSoup</h2><p><code>BeautifulSoup</code> turns HTML text into a tree you can search. You hand it the raw HTML and a parser name:</p><pre><code><code>from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")
</code></code></pre><p>The two most-used methods:</p><ul><li><p><code>soup.find(tag, class_="...")</code> &#8212; returns the <em>first</em> matching element (or <code>None</code>).</p></li><li><p><code>soup.find_all(tag, class_="...")</code> &#8212; returns a <em>list</em> of every matching element.</p></li></ul><p>Note the <code>class_</code> with the trailing underscore &#8212; <code>class</code> is a reserved word in Python, so BeautifulSoup uses <code>class_</code> for matching on CSS classes. It catches everyone the first time.</p><h2>Understanding find_all for Repeated Items</h2><p>The first real scraping move is grabbing every book card on the page. We saw each one is an <code>&lt;article class="product_pod"&gt;</code>, so:</p><pre><code><code>book_cards = soup.find_all("article", class_="product_pod")

print(f"Found {len(book_cards)} books")
# Found 20 books
</code></code></pre><p><code>find_all</code> returns a list &#8212; we now iterate through it and extract from each card individually. This is the <em>heart</em> of catalog scraping: find the repeating container with <code>find_all</code>, then operate on each one in a loop.</p><h2>Understanding Field Extraction</h2><p>Inside each <code>&lt;article&gt;</code>, we pull out the four fields. Each one is its own little trick:</p><p><strong>Title</strong> &#8212; from the <code>&lt;a&gt;</code> tag inside the <code>&lt;h3&gt;</code>. The visible link text is truncated (&#8221;A Light in the &#8230;&#8221;), but the full title is in the <code>title</code> attribute:</p><pre><code><code>title_link = card.find("h3").find("a")
title = title_link["title"]           # "A Light in the Attic"  (the full one)
</code></code></pre><p><strong>Price</strong> &#8212; inside <code>&lt;p class="price_color"&gt;</code>, including the <code>&#163;</code> symbol. We extract the text, strip the symbol, convert to a float:</p><pre><code><code>price_text = card.find("p", class_="price_color").get_text()
price = float(price_text.replace("&#163;", "").strip())
</code></code></pre><p><strong>Rating</strong> &#8212; in the <em>class name</em> of <code>&lt;p class="star-rating Three"&gt;</code>. The number is encoded as a word:</p><pre><code><code>rating_tag = card.find("p", class_="star-rating")
rating_word = rating_tag["class"][1]       # ["star-rating", "Three"] -&gt; "Three"
rating = RATING_MAP[rating_word]           # {"One":1, "Two":2, ...}
</code></code></pre><p><strong>Availability</strong> &#8212; inside <code>&lt;p class="instock availability"&gt;</code>. The text has leading/trailing whitespace:</p><pre><code><code>availability = card.find("p", class_="instock availability").get_text(strip=True)
# "In stock"
</code></code></pre><p>Each extraction is two or three lines, but together they capture everything we need. <strong>The pattern is always the same: locate, extract, clean.</strong></p><h2>Understanding Why title= Beats the Link Text</h2><p>You might be tempted to use the visible text &#8212; <code>title_link.get_text()</code> &#8212; for the title. Don&#8217;t. The visible text is <em>deliberately truncated</em>: &#8220;A Light in the &#8230;&#8221; instead of &#8220;A Light in the Attic.&#8221;</p><p>The full title lives in the <code>title</code> attribute of the same link. Why? Because that&#8217;s what websites use to show a tooltip when you hover. The HTML stores the real value there even when the visible text is shortened for layout.</p><p><strong>Lesson:</strong> when scraping, the visible text isn&#8217;t always the data you want. Inspect the HTML, find the cleanest source, and use that. Attribute values (<code>title</code>, <code>data-*</code>, <code>value</code>, <code>href</code>) often hold the real, untruncated information.</p><h2>Understanding Resolving Relative URLs</h2><p>The <code>href</code> on each book is <em>relative</em>: <code>catalogue/a-light-in-the-attic_1000/index.html</code>. To save a useful link, we need the <em>absolute</em> URL. <code>urllib.parse.urljoin</code> handles this correctly even when the page has a base path:</p><pre><code><code>from urllib.parse import urljoin

base_url = "https://books.toscrape.com/"
relative = card.find("h3").find("a")["href"]

absolute = urljoin(base_url, relative)
# 'https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html'
</code></code></pre><p>Manual string concatenation breaks on edge cases. <code>urljoin</code> always produces a correct URL &#8212; use it without thinking about it.</p><h2>Understanding Saving to CSV</h2><p>We could use pandas, but the standard library&#8217;s <code>csv</code> module is perfect for clean, structured data:</p><pre><code><code>import csv

with open("books.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "price", "rating",
                                            "availability", "url"])
    writer.writeheader()
    writer.writerows(books)
</code></code></pre><p>Two pieces of CSV trivia worth knowing:</p><ul><li><p><code>newline=""</code> &#8212; without this, the <code>csv</code> module sometimes writes blank lines between rows on Windows. Always include it.</p></li><li><p><code>encoding="utf-8"</code> &#8212; book titles can include accented characters, em-dashes, smart quotes. UTF-8 handles them all; the default encoding doesn&#8217;t on every OS.</p></li></ul><p><code>DictWriter</code> is the natural match for our extracted books &#8212; each book is already a dict with the right keys.</p><h2>Understanding Polite Scraping</h2><p>books.toscrape.com explicitly invites scraping (&#8221;We love being scraped!&#8221;), so today&#8217;s single fetch is fine. But the habits start now: identify yourself in the User-Agent, set a timeout, and check <code>raise_for_status</code>. Tomorrow when we hit 50 pages in a row, we&#8217;ll add a delay between requests too &#8212; but that&#8217;s a Day 2 detail. Today: one polite request, gracefully handled.</p><h2>Practical Use Cases</h2><p><strong>1. Catalog extraction:</strong></p><pre><code><code>The same pattern (find_all the cards, extract per card) scrapes any e-commerce listing page.
</code></code></pre><p><strong>2. Job listings:</strong></p><pre><code><code>Sites like Indeed and Remote OK use repeated cards just like book listings.
</code></code></pre><p><strong>3. Real-estate aggregation:</strong></p><pre><code><code>Pull property cards from a listing page &#8212; same logic, different selectors.
</code></code></pre><p><strong>4. Research and data collection:</strong></p><pre><code><code>Academic papers, datasets, contest leaderboards &#8212; catalog patterns are everywhere.
</code></code></pre><p><strong>5. Foundation for Day 2:</strong></p><pre><code><code>Once you can scrape one page, scaling to many is just a loop with pagination.
</code></code></pre><h2>Coming Tomorrow</h2><p>Tomorrow we go from 20 books to <strong>1,000</strong>. You&#8217;ll follow the &#8220;next page&#8221; link, scrape all 50 pages of the catalog, and add detail-page scraping for richer data &#8212; UPC, exact stock count, full description, and category. We also add polite delays between requests because real scrapers don&#8217;t hammer servers.</p><h2>Skeleton and Solution</h2><p>Below you will find both a downloadable skeleton.py file to help you code the project with comment guides and the downloadable solution.py file containing the correct solution.</p><p>Get the code skeleton here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/view/W8-gfZyx_tECBctPu4ODfA&quot;,&quot;text&quot;:&quot;View Skeleton&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/view/W8-gfZyx_tECBctPu4ODfA"><span>View Skeleton</span></a></p><p></p><p>Get the code solution here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/evolution/LuKHP5dv5mE23mZJLIeYlw&quot;,&quot;text&quot;:&quot;View Code Solution&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/evolution/LuKHP5dv5mE23mZJLIeYlw"><span>View Code Solution</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Build a CSV Sales Toolkit : Day 3 - Batch CSV Processor]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/build-a-csv-sales-toolkit-day-3-batch</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-csv-sales-toolkit-day-3-batch</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Fri, 05 Jun 2026 14:37:37 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/65cdb943-46a1-4ad6-bdea-5bdcc29ae6ec_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>CSV Sales Toolkit</strong> that cleans messy data, generates insightful reports, and processes whole folders of files at once &#8212; the exact workflow of real data work.</p><ul><li><p><strong>Day 1:</strong> Visual CSV Cleaner</p></li><li><p><strong>Day 2:</strong> Sales Analyzer &amp; Report</p></li><li><p><strong>Day 3:</strong> Batch CSV Processor <strong>(Today)</strong></p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-20">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p><strong>Welcome to the finale.</strong> You have a cleaner and an analyzer. Today they grow up. Instead of working on one file at a time, today&#8217;s tool points at a <strong>folder of monthly sales CSVs</strong>, cleans every one, combines them all into a single dataset, and produces a <strong>consolidated multi-month report</strong> &#8212; with per-month comparisons, growth percentages, and a full-period summary.</p><p>This is the moment your script becomes a <em>tool</em> someone could automate. Drop a new month&#8217;s file into the folder, run the script, get the updated report. That&#8217;s how real data work scales.</p><h2>Project Task</h2><p>Build a batch CSV processor that:</p><ul><li><p>Discovers every CSV in a designated folder automatically</p></li><li><p>Cleans each file using the Day 1 cleaning pipeline</p></li><li><p>Reports the result of each file (rows, missing values fixed)</p></li><li><p>Continues to the next file if one fails, instead of crashing</p></li><li><p>Merges all cleaned data into a single combined DataFrame</p></li><li><p>Runs the Day 2 analysis on the combined data</p></li><li><p>Adds per-month aggregates (revenue per month, month-over-month change)</p></li><li><p>Identifies the best month, top product overall, and trends</p></li><li><p>Saves both the combined CSV and the consolidated report</p></li><li><p>Prints a progress summary as it works</p></li></ul><p>This project gives you hands-on practice with <code>pathlib</code> for folder scanning, batch file handling, robust error handling, <code>pd.concat</code> for combining DataFrames, time-grouped aggregations with <code>dt.to_period</code>, and turning your earlier scripts into reusable functions.</p><h2>Expected Output</h2><p><strong>Folder structure before running:</strong></p><pre><code><code>monthly_sales/
&#9500;&#9472;&#9472; sales_2026_01.csv     (30 orders, messy)
&#9500;&#9472;&#9472; sales_2026_02.csv     (25 orders, messy)
&#9500;&#9472;&#9472; sales_2026_03.csv     (27 orders, messy)
&#9492;&#9472;&#9472; sales_2026_04.csv     (28 orders, messy)
</code></code></pre><p><strong>Running the processor:</strong></p><pre><code><code>python batch_process.py
</code></code></pre><p><strong>Console Output:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T6a8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T6a8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png 424w, https://substackcdn.com/image/fetch/$s_!T6a8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png 848w, https://substackcdn.com/image/fetch/$s_!T6a8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png 1272w, https://substackcdn.com/image/fetch/$s_!T6a8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T6a8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png" width="1372" height="3612" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3612,&quot;width&quot;:1372,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:597803,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199481329?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!T6a8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png 424w, https://substackcdn.com/image/fetch/$s_!T6a8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png 848w, https://substackcdn.com/image/fetch/$s_!T6a8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png 1272w, https://substackcdn.com/image/fetch/$s_!T6a8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb886998e-f648-4b46-9767-4656ea932ec7_1372x3612.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>You drop a fifth month into the folder next quarter &#8212; and rerunning produces an updated five-month report with no changes to the code. That&#8217;s the win.</p><h2>Setup Instructions</h2><p><strong>Install pandas:</strong></p><pre><code><code>pip install pandas
</code></code></pre><p><strong>Get the data files:</strong></p><p>Download the <code>monthly_sales/</code> folder:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://drive.google.com/file/d/1ZM1--zluyoqXJcqMVRFXn_Fzc7GI-Rfs/view?usp=sharing&quot;,&quot;text&quot;:&quot;Download&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://drive.google.com/file/d/1ZM1--zluyoqXJcqMVRFXn_Fzc7GI-Rfs/view?usp=sharing"><span>Download</span></a></p><p></p><p>It contains four monthly CSVs. Place the folder next to your script.</p><p><strong>Run it:</strong></p><pre><code><code>python batch_process.py
</code></code></pre><p>The script reads every CSV in <code>monthly_sales/</code> and writes two new files: <code>all_sales_clean.csv</code> (the merged clean data) and <code>consolidated_report.txt</code> (the report).</p><h2>Understanding Folder Scanning with pathlib</h2><p>The first batch trick is <strong>finding</strong> the files automatically. We don&#8217;t hardcode filenames &#8212; we ask the folder what&#8217;s in it. <code>pathlib</code> makes this elegant:</p><pre><code><code>from pathlib import Path

folder = Path("monthly_sales")
csv_files = sorted(folder.glob("*.csv"))

print(f"Found {len(csv_files)} CSV files")
for path in csv_files:
    print(f"  {path.name}")
</code></code></pre><p>Three things to know:</p><ul><li><p><code>Path("monthly_sales")</code> &#8212; pathlib&#8217;s modern, OS-aware way to point at a folder.</p></li><li><p><code>.glob("*.csv")</code> &#8212; returns every file matching the pattern.</p></li><li><p><code>sorted(...)</code> &#8212; files come back in filesystem order, which can vary. Sorting alphabetically gives you predictable chronological order when filenames include dates.</p></li></ul><p>Adding a new monthly file later is now a non-event. The script discovers it automatically.</p><h2>Understanding Reusable Cleaning Functions</h2><p>We need the Day 1 cleaning logic &#8212; but as a <em>function</em>, not a GUI button. So we lift it into a standalone helper:</p><pre><code><code>TEXT_COLUMNS = ["customer_name", "product", "category", "status"]

def clean_dataframe(df):
    """Apply every cleaning step from Day 1 and return the result."""
    for col in TEXT_COLUMNS:
        df[col] = df[col].astype(str).str.strip()

    df["category"] = df["category"].str.title()
    df["status"] = df["status"].str.lower()

    df["unit_price"] = (
        df["unit_price"].astype(str)
        .str.replace("$", "", regex=False)
        .str.replace(",", "", regex=False)
    )
    df["unit_price"] = pd.to_numeric(df["unit_price"], errors="coerce")

    df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
    df["quantity"] = df["quantity"].fillna(1).astype(int)

    category_avg = df.groupby("category")["unit_price"].transform("mean")
    df["unit_price"] = df["unit_price"].fillna(category_avg).round(2)

    df["total_price"] = (df["quantity"] * df["unit_price"]).round(2)

    return df
</code></code></pre><p>This is the <strong>same logic as Day 1</strong>, packaged so it can be called over and over. That&#8217;s the broader lesson: <strong>a function you wrote for one file works for a thousand, the moment you stop hardcoding the data and start passing it as an argument.</strong></p><h2>Understanding the Batch Loop</h2><p>The heart of batch processing is the loop. For each file: load, clean, collect. The key habit is <strong>defensive error handling</strong> &#8212; one bad file should not destroy the entire run:</p><pre><code><code>cleaned_dfs = []
failures = []

for i, path in enumerate(csv_files, 1):
    try:
        df = pd.read_csv(path)
        df = clean_dataframe(df)
        cleaned_dfs.append(df)
        print(f"  [{i}/{len(csv_files)}] {path.name}   &#10003;  {len(df)} rows cleaned")
    except Exception as e:
        failures.append((path.name, str(e)))
        print(f"  [{i}/{len(csv_files)}] {path.name}   &#10007;  {e}")
</code></code></pre><p>Two important moves:</p><ul><li><p><code>try/except</code><strong> around each file</strong> &#8212; if one CSV is corrupt or has unexpected columns, the loop logs the failure and continues. The other files still process.</p></li><li><p><code>failures</code><strong> list</strong> &#8212; collecting errors instead of letting them kill the script means you get a <em>complete</em> picture of what happened, even when some files broke.</p></li></ul><p>A batch tool that crashes on the third file out of fifty is barely better than running them manually. Robustness is the feature.</p><h2>Understanding pd.concat for Merging</h2><p>After cleaning, we combine the per-file DataFrames into one. <code>pd.concat</code> is the tool:</p><pre><code><code>combined = pd.concat(cleaned_dfs, ignore_index=True)
combined.to_csv("all_sales_clean.csv", index=False)
</code></code></pre><p><code>pd.concat</code> stacks DataFrames vertically &#8212; row after row &#8212; as long as they share columns. <code>ignore_index=True</code> is important: without it, you keep each file&#8217;s original row indexes (0, 1, 2... then 0, 1, 2... again). With it, you get a single fresh sequence &#8212; much easier to work with.</p><p>The merged file is <em>also</em> a deliverable. Now you have a single clean CSV holding every month&#8217;s data, ready for any future analysis.</p><h2>Understanding Period Grouping</h2><p>The headline new capability today is <strong>monthly comparisons</strong>. With the merged data, we can group by month using pandas&#8217; time-period feature:</p><pre><code><code>combined["order_date"] = pd.to_datetime(combined["order_date"])

monthly = (
    completed.assign(month=completed["order_date"].dt.to_period("M"))
    .groupby("month")
    .agg(
        orders=("order_id", "count"),
        revenue=("total_price", "sum"),
    )
)
</code></code></pre><p>Two new ideas here:</p><ul><li><p><code>.dt.to_period("M")</code> &#8212; converts a date like <code>2026-01-15</code> into the period <code>2026-01</code>. Every date in January becomes the same period, which is exactly what you need to group by month.</p></li><li><p><code>.agg(name=(column, function), ...)</code> &#8212; the <em>named-aggregation</em> form of <code>agg</code>. You get to <em>name</em> each output column and pick what it&#8217;s computed from, in one clean call. Much more readable than the older positional form.</p></li></ul><p>The result is a small DataFrame with one row per month, ready to compare.</p><h2>Understanding Month-over-Month Change</h2><p>A monthly total tells you what happened. A month-over-month <em>change</em> tells you the <em>direction</em>. Pandas&#8217; <code>pct_change()</code> makes this a one-liner:</p><pre><code><code>monthly["change"] = monthly["revenue"].pct_change() * 100
# Output:
# 2026-01     &#8212;      (first month, no prior to compare to)
# 2026-02   -2.7%
# 2026-03  +15.3%
# 2026-04   +5.9%
</code></code></pre><p><code>pct_change()</code> returns the percentage change between each row and the one before it. The first row gets <code>NaN</code> &#8212; there&#8217;s no earlier value to compare to &#8212; which we display as an em-dash.</p><p>This single column turns a list of monthly revenues into a <em>story</em>: &#8220;Dipped in Feb, surged 15% in March, and kept climbing.&#8221; That&#8217;s the kind of sentence reports are supposed to enable.</p><h2>Understanding Formatting the Monthly Table</h2><p>The monthly section needs careful column alignment to look like a real report:</p><pre><code><code>lines.append("  Month        Orders   Revenue       Change")
lines.append("  " + "-" * 48)

for month, row in monthly.iterrows():
    change = row["change"]
    if pd.isna(change):
        change_str = "    &#8212;"            # first month, nothing to compare to
    else:
        sign = "+" if change &gt;= 0 else ""
        change_str = f"{sign}{change:.1f}%"

    lines.append(
        f"  {str(month):&lt;10}"
        f"  {int(row['orders']):&gt;4}"
        f"     ${row['revenue']:&gt;8,.2f}"
        f"   {change_str:&gt;6}"
    )
</code></code></pre><p>A few details earn their place:</p><ul><li><p><code>pd.isna(change)</code> &#8212; the safe way to check for missing values. (<code>change == NaN</code> always returns False &#8212; never use <code>==</code> for NaN.)</p></li><li><p><code>"+" if change &gt;= 0 else ""</code> &#8212; manually prepending <code>+</code> makes positive numbers visually distinct from negative ones at a glance.</p></li><li><p><code>{...:&lt;10}</code><strong> / </strong><code>{...:&gt;4}</code><strong> / </strong><code>{...:&gt;8,.2f}</code> &#8212; left- and right-alignment with widths. The column headers and the data rows use the same widths, so everything lines up.</p></li></ul><p>These small touches turn raw numbers into a table that reads like something from a real product.</p><h2>Understanding Why This Architecture Works</h2><p>Look at the three days side by side:</p><ul><li><p><strong>Day 1</strong> built a <code>clean_dataframe</code> workflow inside a GUI.</p></li><li><p><strong>Day 2</strong> built <code>build_report</code> from clean data.</p></li><li><p><strong>Day 3</strong> wraps both in a loop over a folder.</p></li></ul><p>Each layer was rewritten exactly <em>once</em>: when its job changed. The cleaning logic didn&#8217;t change today &#8212; we just call it in a loop. The reporting structure didn&#8217;t change much either &#8212; we added monthly comparisons. <strong>Most of today&#8217;s code is glue: scanning a folder, iterating, concatenating, formatting.</strong> That&#8217;s a real pattern for building software: solve one case well, then loop the solution.</p><h2>Practical Use Cases</h2><p><strong>1. Monthly business reporting:</strong></p><pre><code><code>Drop each new month's export into the folder, rerun, get an updated report &#8212; forever.
</code></code></pre><p><strong>2. Year-end consolidation:</strong></p><pre><code><code>Process 12 monthly files into one yearly summary, with month-over-month growth.
</code></code></pre><p><strong>3. Multi-source data merging:</strong></p><pre><code><code>Combine exports from different stores, regions, or systems into one analysis.
</code></code></pre><p><strong>4. Building data pipelines:</strong></p><pre><code><code>The pattern (discover &#8594; clean &#8594; merge &#8594; analyze) is the skeleton of every ETL job.
</code></code></pre><p><strong>5. Automated jobs:</strong></p><pre><code><code>Schedule this script and it generates fresh reports nightly without supervision.
</code></code></pre><h2>What You&#8217;ve Accomplished This Week</h2><p>&#127881; <strong>Congratulations!</strong> You&#8217;ve built a complete <strong>CSV Sales Toolkit</strong>.</p><ul><li><p><strong>Day 1:</strong> Visual CSV cleaner &#8212; fixes real-world data mess with a click</p></li><li><p><strong>Day 2:</strong> Sales analyzer &#8212; turns clean data into business insights</p></li><li><p><strong>Day 3:</strong> Batch processor &#8212; scales the whole pipeline to a folder of files</p></li></ul><p><strong>You now have:</strong></p><p>&#9989; <strong>Data cleaning skills</strong> &#8212; whitespace, casing, types, missing values, the real toolkit &#9989; <strong>Aggregation skills</strong> &#8212; <code>groupby</code>, <code>agg</code>, <code>pct_change</code>, period grouping &#9989; <strong>Batch processing skills</strong> &#8212; <code>pathlib</code>, looped error handling, <code>pd.concat</code> &#9989; <strong>Report formatting skills</strong> &#8212; alignment, currency, percent, multi-section output &#9989; <strong>A clean architecture</strong> &#8212; three layers, reused not rewritten</p><p><strong>Real-world applications:</strong></p><ul><li><p>&#128202; <strong>Sales and finance reporting</strong> &#8212; exactly the workflow this teaches</p></li><li><p>&#129534; <strong>Receipt or invoice consolidation</strong> &#8212; batch-clean and merge</p></li><li><p>&#127991;&#65039; <strong>Inventory analysis</strong> &#8212; same patterns apply to product/stock data</p></li><li><p>&#128200; <strong>Marketing reporting</strong> &#8212; clean campaign exports, aggregate, compare</p></li><li><p>&#129302; <strong>Personal automation</strong> &#8212; turn a 30-minute manual job into a 10-second script</p></li></ul><p><strong>Next steps:</strong></p><ul><li><p>Add charts: matplotlib bar/line plots embedded in a PDF report</p></li><li><p>Make it incremental: detect new files and only process those</p></li><li><p>Schedule it: run nightly with cron, send the report by email</p></li><li><p>Add forecasting: use the monthly trend to predict next month</p></li><li><p>Database storage: write the cleaned merged data into SQLite for queries</p></li></ul><p>You&#8217;ve built the foundation for a <strong>real data automation pipeline</strong>. </p><h2>View Code Evolution</h2><p>Compare today&#8217;s batch processor with the Day 1 cleaner and Day 2 analyzer &#8212; and see how a folder loop, robust error handling, and one good architecture turn a small script into a tool that scales.</p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/build-a-csv-sales-toolkit-day-3-batch">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build a CSV Sales Toolkit : Day 2 - Sales Analyzer & Report]]></title><description><![CDATA[Today we build a Python program that loads a messy CSV, see it in a table, click a button, and watch the data transform &#8212; bad values fixed, missing cells filled.]]></description><link>https://dailypythonprojects.substack.com/p/build-a-csv-sales-toolkit-day-2-sales</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-csv-sales-toolkit-day-2-sales</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Wed, 03 Jun 2026 12:51:02 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/32a57b28-319d-4e32-a1f9-345c7d50661e_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>CSV Sales Toolkit</strong> that cleans messy data, generates insightful reports, and processes whole folders of files at once &#8212; the exact workflow of real data work.</p><ul><li><p><strong>Day 1:</strong> Visual CSV Cleaner</p></li><li><p><strong>Day 2:</strong> Sales Analyzer &amp; Report <strong>(Today)</strong></p></li><li><p><strong>Day 3:</strong> Batch CSV Processor</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-20">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>Yesterday we cleaned the data. Today we <strong>make it talk</strong>. Clean data is only useful if it tells you something &#8212; so today we build a Sales Analyzer that takes <code>orders_clean.csv</code> and computes the numbers a business actually cares about: total revenue, best-selling products, revenue by category, daily trends, and average order value.</p><p>The result is a formatted text report you can read at a glance or email to someone &#8212; turning a spreadsheet of rows into actual answers.</p><h2>Project Task</h2><p>Build a sales analyzer that:</p><ul><li><p>Loads the cleaned orders CSV from Day 1</p></li><li><p>Separates completed orders from cancelled ones</p></li><li><p>Calculates headline metrics: total revenue, order count, average order value</p></li><li><p>Finds the best-selling products (by quantity and by revenue)</p></li><li><p>Breaks revenue down by category</p></li><li><p>Computes a daily sales trend</p></li><li><p>Calculates the cancellation rate</p></li><li><p>Formats everything into a clean, readable report</p></li><li><p>Saves the report to a text file</p></li></ul><p>This project gives you hands-on practice with pandas aggregation, <code>groupby</code>, sorting, filtering, and turning raw numbers into a formatted business report &#8212; the analysis half of real data work.</p><h2>Expected Output</h2><p><strong>Running the analyzer:</strong></p><pre><code><code>python analyze_sales.py</code></code></pre><p><strong>Console Output (and saved report):</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B06E!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B06E!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png 424w, https://substackcdn.com/image/fetch/$s_!B06E!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png 848w, https://substackcdn.com/image/fetch/$s_!B06E!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png 1272w, https://substackcdn.com/image/fetch/$s_!B06E!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B06E!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:550235,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199456161?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B06E!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png 424w, https://substackcdn.com/image/fetch/$s_!B06E!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png 848w, https://substackcdn.com/image/fetch/$s_!B06E!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png 1272w, https://substackcdn.com/image/fetch/$s_!B06E!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ed14b2c-f634-4cf9-9001-8e894affef18_1488x992.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>The same text is printed to the console and saved as <code>sales_report.txt</code> &#8212; readable, shareable, done.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qj7M!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qj7M!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png 424w, https://substackcdn.com/image/fetch/$s_!qj7M!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png 848w, https://substackcdn.com/image/fetch/$s_!qj7M!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png 1272w, https://substackcdn.com/image/fetch/$s_!qj7M!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qj7M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:949100,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199456161?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qj7M!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png 424w, https://substackcdn.com/image/fetch/$s_!qj7M!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png 848w, https://substackcdn.com/image/fetch/$s_!qj7M!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png 1272w, https://substackcdn.com/image/fetch/$s_!qj7M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a580d83-a383-443b-aff1-b43fd6e04f1f_2964x1976.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Setup Instructions</h2><p><strong>Install pandas:</strong></p><pre><code><code>pip install pandas
</code></code></pre><p><strong>Get the data file:</strong></p><p>You need <code>orders_clean.csv</code> &#8212; the output of Day 1. Run the Day 1 cleaner first, or use the cleaned file provided below:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://drive.google.com/file/d/1uY6BirMazbQiQOw9HtC9qHwvJynC0G2H/view?usp=sharing&quot;,&quot;text&quot;:&quot;Download orders_clean.csv&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://drive.google.com/file/d/1uY6BirMazbQiQOw9HtC9qHwvJynC0G2H/view?usp=sharing"><span>Download orders_clean.csv</span></a></p><p></p><p>Place the <em>orders_clean.csv</em> file in the same folder as the script.</p><p><strong>Run the Python script:</strong></p><pre><code><code>python analyze_sales.py</code></code></pre><p>The script reads <code>orders_clean.csv</code> and writes <code>sales_report.txt</code>.</p><h2>Understanding Why Clean Data Matters Here</h2><p>Day 1 wasn&#8217;t busywork &#8212; today is where it pays off. Every calculation below depends on the cleaning:</p><ul><li><p><strong>Total revenue</strong> sums <code>total_price</code> &#8212; only possible because prices are real numbers, not <code>$12.99</code> text.</p></li><li><p><strong>Revenue by category</strong> groups by <code>category</code> &#8212; only gives 3 groups because we standardized the capitalization.</p></li><li><p><strong>Completed vs. cancelled</strong> filters on <code>status</code> &#8212; only works because we made the status values consistent.</p></li></ul><p>Analysis on dirty data produces <em>confidently wrong</em> answers. That&#8217;s worse than no answer. Clean first, analyze second &#8212; always.</p><h2>Understanding Loading and Filtering</h2><p>We start by loading the clean CSV and splitting it by status. Most metrics should count <em>completed</em> orders only &#8212; a cancelled order earned no revenue:</p><pre><code><code>import pandas as pd

df = pd.read_csv("orders_clean.csv")

# Split by status
completed = df[df["status"] == "completed"]
cancelled = df[df["status"] == "cancelled"]
</code></code></pre><p><code>df[df["status"] == "completed"]</code> is <strong>boolean filtering</strong> &#8212; the inner part builds a column of <code>True</code>/<code>False</code>, and the outer part keeps only the <code>True</code> rows. This is the single most common pandas operation, and it&#8217;s how we make sure cancelled orders don&#8217;t inflate revenue.</p><h2>Understanding Headline Metrics</h2><p>The top-level numbers are simple aggregations on the completed orders:</p><pre><code><code>total_revenue = completed["total_price"].sum()
order_count = len(completed)
items_sold = completed["quantity"].sum()

# Average order value &#8212; guard against dividing by zero
average_order = total_revenue / order_count if order_count else 0

# Cancellation rate as a percentage
cancellation_rate = len(cancelled) / len(df) * 100
</code></code></pre><p>Each metric is one line. <code>.sum()</code> adds a column, <code>len()</code> counts rows. The only subtlety is <strong>guarding the division</strong> &#8212; <code>if order_count else 0</code> prevents a crash if there are no completed orders. Defensive arithmetic like this is what separates a script that works on <em>your</em> data from one that works on <em>any</em> data.</p><h2>Understanding groupby for Category Revenue</h2><p>To break revenue down by category, we use <code>groupby</code> &#8212; pandas&#8217; tool for &#8220;split into groups, then calculate per group&#8221;:</p><pre><code><code>category_revenue = (
    completed.groupby("category")["total_price"]
    .sum()
    .sort_values(ascending=False)
)
</code></code></pre><p>Read it as a pipeline:</p><ol><li><p><code>groupby("category")</code> &#8212; split the rows into one group per category.</p></li><li><p><code>["total_price"]</code> &#8212; we only care about the price column.</p></li><li><p><code>.sum()</code> &#8212; total it within each group.</p></li><li><p><code>.sort_values(ascending=False)</code> &#8212; order the result, biggest first.</p></li></ol><p>The result is a <code>Series</code>: category names on the left, total revenue on the right. This four-step pattern &#8212; <em>group, select, aggregate, sort</em> &#8212; is the backbone of nearly every report you&#8217;ll ever write.</p><h2>Understanding Top Products</h2><p>&#8220;Best-selling&#8221; has two meanings, and a good report shows both. Some products sell in high <em>volume</em>; others bring in more <em>money</em>. Same <code>groupby</code> pattern, different column:</p><pre><code><code># By revenue &#8212; which products earn the most money
top_by_revenue = (
    completed.groupby("product")["total_price"]
    .sum()
    .sort_values(ascending=False)
    .head(5)
)

# By quantity &#8212; which products sell the most units
top_by_quantity = (
    completed.groupby("product")["quantity"]
    .sum()
    .sort_values(ascending=False)
    .head(5)
)
</code></code></pre><p><code>.head(5)</code> keeps only the top five after sorting. Showing both lists is genuinely useful: a cheap product can top the quantity list while barely registering on revenue &#8212; that contrast is an <em>insight</em>, not just a number.</p><h2>Understanding the Daily Trend</h2><p>To see sales over time, we group by date. The <code>order_date</code> column is text, so we convert it to real dates first &#8212; that makes it sort chronologically and behave correctly:</p><pre><code><code>completed = completed.copy()
completed["order_date"] = pd.to_datetime(completed["order_date"])

daily_sales = (
    completed.groupby("order_date")["total_price"]
    .sum()
    .sort_index()
)

# The single best day
best_day = daily_sales.idxmax()       # the date with the highest total
best_amount = daily_sales.max()        # that highest total
</code></code></pre><p>Two new tools here:</p><ul><li><p><code>pd.to_datetime</code> turns date <em>text</em> into real date objects, so <code>2026-01-09</code> sorts after <code>2026-01-08</code>, not alphabetically.</p></li><li><p><code>.idxmax()</code> returns the <em>index label</em> of the maximum value &#8212; here, the date of the best day &#8212; while <code>.max()</code> returns the value itself. The pair answers &#8220;what was the best day, and how good was it?&#8221;</p></li></ul><blockquote><p><strong>Note the </strong><code>.copy()</code> &#8212; when you take a filtered slice of a DataFrame and then modify it, pandas warns you. <code>.copy()</code> makes a clean, independent copy so the modification is unambiguous and warning-free.</p></blockquote><h2>Understanding Formatting the Report</h2><p>Raw numbers aren&#8217;t a report &#8212; <em>formatting</em> makes them readable. Python&#8217;s f-strings give you fine control over alignment and number display:</p><pre><code><code># Currency: 2 decimals, comma thousands separator
print(f"  Total revenue:  ${total_revenue:,.2f}")
# -&gt; "  Total revenue:  $986.40"

# Percentage: 1 decimal place
print(f"  Cancellation rate:  {cancellation_rate:.1f}%")
# -&gt; "  Cancellation rate:  20.0%"

# Aligned columns: pad the name to a fixed width
for product, revenue in top_by_revenue.items():
    print(f"  {product:&lt;20} ${revenue:&gt;9,.2f}")
</code></code></pre><p>The format codes earn their keep:</p><ul><li><p><code>:,.2f</code> &#8212; comma thousands separator, exactly 2 decimals (money).</p></li><li><p><code>:.1f</code> &#8212; 1 decimal place (percentages).</p></li><li><p><code>:&lt;20</code> &#8212; left-align in a 20-character space (product names line up).</p></li><li><p><code>:&gt;9</code> &#8212; right-align in a 9-character space (numbers line up by the decimal).</p></li></ul><p>Aligned columns are the difference between a report that looks professional and one that looks like a data dump.</p><h2>Understanding Building Report Sections</h2><p>Rather than <code>print</code> scattered everywhere, we build the report as a list of text lines, then join it once at the end. This way the <em>same text</em> goes to both the console and the file:</p><pre><code><code>def build_report(df):
    lines = []
    lines.append("=" * 80)
    lines.append("SALES ANALYSIS REPORT")
    lines.append("=" * 80)
    lines.append("")
    # ... append every section ...
    return "\n".join(lines)
</code></code></pre><p>Collecting lines in a list and <code>"\n".join()</code>-ing them at the end is cleaner than dozens of <code>print</code> calls. You build the report <em>once</em>, as a string &#8212; then you can print it, save it, or both, with no duplication.</p><h2>Understanding Saving the Report</h2><p>Writing the finished report to a text file is a few lines with a <code>with</code> block:</p><pre><code><code>report_text = build_report(df)

print(report_text)                       # show it on screen

with open("sales_report.txt", "w") as f: # and save it to a file
    f.write(report_text)
</code></code></pre><p>The <code>with open(...)</code> block handles the file safely &#8212; it&#8217;s automatically closed even if something goes wrong. Because <code>build_report</code> returned one string, sending it to <em>both</em> the screen and the file is trivial. A <code>.txt</code> report is something a non-technical person can open, read, and forward &#8212; which is often the whole point of doing the analysis.</p><h2>Practical Use Cases</h2><p><strong>1. Weekly or monthly sales summaries:</strong></p><pre><code><code>Run the analyzer on the latest data, get a report ready to send to your team.
</code></code></pre><p><strong>2. Spotting your best products:</strong></p><pre><code><code>The revenue vs. quantity lists reveal which products to promote or restock.
</code></code></pre><p><strong>3. Tracking cancellation problems:</strong></p><pre><code><code>A rising cancellation rate is an early warning worth watching.
</code></code></pre><p><strong>4. Finding peak sales days:</strong></p><pre><code><code>The daily trend shows when customers buy &#8212; useful for planning promotions.
</code></code></pre><p><strong>5. Foundation for Day 3:</strong></p><pre><code><code>Tomorrow we run this analysis across a whole folder of monthly files at once.
</code></code></pre><h2>Coming Tomorrow</h2><p>Tomorrow is the finale: the <strong>Batch CSV Processor</strong>. Instead of one file, you&#8217;ll point the tool at a <em>folder</em> of monthly sales CSVs &#8212; it cleans every one, analyzes them all, merges the data, and produces a single consolidated multi-month report. Real batch processing.</p><h2>View Code Evolution</h2><p>Compare today&#8217;s analyzer with yesterday&#8217;s cleaner and see how clean, typed data turns directly into meaningful business metrics.</p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/build-a-csv-sales-toolkit-day-2-sales">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build a CSV Sales Toolkit : Day 1 - CSV Sales Cleaner]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/build-a-csv-sales-toolkit-day-1-csv</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-csv-sales-toolkit-day-1-csv</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Tue, 02 Jun 2026 12:36:52 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/fe074992-e8fb-4549-9c59-1474564d4325_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>CSV Sales Toolkit</strong> that cleans messy data, generates insightful reports, and processes whole folders of files at once &#8212; the exact workflow of real data work.</p><p><strong>Why build this?</strong> Because real-world data is <em>always</em> messy. Inconsistent capitalization, stray whitespace, dollar signs in number columns, missing values &#8212; every dataset has them. The ability to reliably clean data is one of the most practical skills in all of Python.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong>What you&#8217;ll learn:</strong> This series teaches you data cleaning, type conversion, missing-value handling, aggregation and reporting, and batch file processing &#8212; the core skills behind data analysis and automation.</p><p><strong>Why this matters:</strong> By Day 3, you&#8217;ll have a tool that points at a folder of monthly sales files, cleans every one, and produces a single consolidated business report. That&#8217;s a genuinely useful piece of software.</p><ul><li><p><strong>Day 1:</strong> Visual CSV Cleaner <strong>(Today)</strong></p></li><li><p><strong>Day 2:</strong> Sales Analyzer &amp; Report</p></li><li><p><strong>Day 3:</strong> Batch CSV Processor</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-20">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>We start at the foundation of all data work: <strong>cleaning</strong>. But instead of a script that runs and prints text, we&#8217;re building a <strong>visual desktop app</strong>. You load a messy CSV, <em>see</em> it in a table, click a button, and watch the data transform &#8212; bad values fixed, missing cells filled, all in front of you.</p><p>The input file looks fine at a glance. But it has stray whitespace, three different spellings of &#8220;Electronics,&#8221; dollar signs stuck in the price column, and missing values. Today you build a tool that fixes all of it &#8212; and <em>shows</em> you the difference.</p><h2>Project Task</h2><p>Build a visual CSV cleaner with Tkinter that:</p><ul><li><p>Loads a raw orders CSV and displays it in a table (Treeview)</p></li><li><p>Shows a &#8220;before&#8221; view of the messy data</p></li><li><p>Cleans the data on a button click:</p><ul><li><p>Strips whitespace from text columns</p></li><li><p>Standardizes category names to Title Case</p></li><li><p>Standardizes order status to lowercase</p></li><li><p>Removes <code>$</code> signs and converts prices to numbers</p></li><li><p>Converts quantity to whole numbers</p></li><li><p>Fills missing values sensibly</p></li><li><p>Adds a calculated <code>total_price</code> column</p></li></ul></li><li><p>Displays the cleaned &#8220;after&#8221; data in the same table</p></li><li><p>Shows a status log of exactly what was cleaned</p></li><li><p>Saves the cleaned data to a new CSV via a Save button</p></li></ul><p>This project gives you hands-on practice with pandas data cleaning, Tkinter GUIs, the Treeview table widget, and connecting a data pipeline to a visual interface &#8212; a practical mix of data and app-building skills.</p><h2>Expected Output</h2><p><strong>Running the app:</strong></p><pre><code><code>python csv_cleaner.py
</code></code></pre><p><strong>Application Window:</strong></p><p>Running the code will produce this desktop app. Here we have already loaded the orders_raw.csv file which you can <a href="https://drive.google.com/file/d/1REpTTz5lzXhXh5fW6vtT3yms3X9jBKRp/view?usp=sharing">download here</a>:</p><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!97BL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!97BL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png 424w, https://substackcdn.com/image/fetch/$s_!97BL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png 848w, https://substackcdn.com/image/fetch/$s_!97BL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png 1272w, https://substackcdn.com/image/fetch/$s_!97BL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!97BL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1317913,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199452299?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!97BL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png 424w, https://substackcdn.com/image/fetch/$s_!97BL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png 848w, https://substackcdn.com/image/fetch/$s_!97BL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png 1272w, https://substackcdn.com/image/fetch/$s_!97BL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03040db-6b9a-4051-8526-b8ab2ca81135_2196x1464.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>After clicking &#8220;Clean Data&#8221; we see this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lo66!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lo66!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png 424w, https://substackcdn.com/image/fetch/$s_!lo66!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png 848w, https://substackcdn.com/image/fetch/$s_!lo66!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png 1272w, https://substackcdn.com/image/fetch/$s_!lo66!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lo66!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1353607,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199452299?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lo66!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png 424w, https://substackcdn.com/image/fetch/$s_!lo66!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png 848w, https://substackcdn.com/image/fetch/$s_!lo66!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png 1272w, https://substackcdn.com/image/fetch/$s_!lo66!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43ecb685-d8d8-4be2-b5fc-d1f84b575bcb_2196x1464.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Watch the table change in place: <code>electronics</code> becomes <code>Electronics</code>, <code>$12.99</code> becomes <code>12.99</code>, the empty quantity in row 1004 fills with <code>1</code>, and a new <code>total_price</code> column appears.</p><h2>Setup Instructions</h2><p><strong>Install pandas:</strong></p><pre><code><code>pip install pandas
</code></code></pre><p>Tkinter ships with Python &#8212; nothing else to install.</p><p><strong>Get the data file:</strong></p><p>Download <code>orders_raw.csv</code> (provided with this project) and keep it handy &#8212; you&#8217;ll load it from inside the app.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://drive.google.com/file/d/1REpTTz5lzXhXh5fW6vtT3yms3X9jBKRp/view?usp=sharing&quot;,&quot;text&quot;:&quot;Download CSV&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://drive.google.com/file/d/1REpTTz5lzXhXh5fW6vtT3yms3X9jBKRp/view?usp=sharing"><span>Download CSV</span></a></p><p><strong>Run the .py program:</strong></p><pre><code><code>python csv_cleaner.py
</code></code></pre><h2>Understanding the Messy Data</h2><p>Before building anything, look at <code>orders_raw.csv</code>. These are the six problems hiding in it:</p><ol><li><p><strong>Whitespace</strong> &#8212; <code>" Alice Johnson"</code> and <code>"Bob Smith "</code> have stray spaces.</p></li><li><p><strong>Inconsistent categories</strong> &#8212; <code>Electronics</code>, <code>electronics</code>, <code>ELECTRONICS</code> are the <em>same</em> category spelled three ways.</p></li><li><p><strong>Inconsistent status</strong> &#8212; <code>completed</code>, <code>Completed</code>, <code>COMPLETED</code> &#8212; same problem.</p></li><li><p><strong>Currency symbols</strong> &#8212; some prices are <code>$12.99</code> (text!) instead of <code>12.99</code> (a number).</p></li><li><p><strong>Missing quantities</strong> &#8212; a few <code>quantity</code> cells are empty.</p></li><li><p><strong>Missing prices</strong> &#8212; a few <code>unit_price</code> cells are empty.</p></li></ol><p>Each one breaks analysis later. Sum revenue and <code>$12.99</code> crashes the math. Group by category and you get six groups instead of three. Cleaning is what makes analysis <em>possible</em>.</p><h2>Understanding the App Structure</h2><p>We organize the whole app as a class. The class holds the DataFrame as <code>self.df</code>, so every method &#8212; load, clean, save &#8212; works on the same shared data:</p><pre><code><code>import tkinter as tk
from tkinter import ttk, filedialog
import pandas as pd

class CSVCleanerApp:
    def __init__(self, root):
        self.root = root
        self.df = None          # the DataFrame, shared across all methods
        self.build_ui()

    def build_ui(self):
        # buttons, table, status log go here
        ...
</code></code></pre><p><code>self.df</code> starts as <code>None</code> (nothing loaded yet). <code>load_csv()</code> fills it, <code>clean_data()</code> transforms it, <code>save_csv()</code> writes it out. Storing the data on <code>self</code> is what lets separate button clicks act on one shared dataset.</p><h2>Understanding the Treeview Table</h2><p>Tkinter&#8217;s <code>ttk.Treeview</code> is the widget for showing tabular data &#8212; rows and columns, like a spreadsheet:</p><pre><code><code># Create the table
self.tree = ttk.Treeview(self.root, show="headings")

# Define which columns exist
self.tree["columns"] = ("order_id", "customer_name", "product")

# Set the header text for each column
for col in self.tree["columns"]:
    self.tree.heading(col, text=col)
    self.tree.column(col, width=120)

# Add a row of data
self.tree.insert("", "end", values=(1001, "Alice Johnson", "Wireless Mouse"))
</code></code></pre><p>Three ideas:</p><ul><li><p><code>show="headings"</code> hides Treeview&#8217;s default empty first column &#8212; you almost always want this.</p></li><li><p><code>.heading()</code> sets the visible column title; <code>.column()</code> sets its width.</p></li><li><p><code>.insert("", "end", values=(...))</code> adds one row; the tuple of values fills the columns left to right.</p></li></ul><h2>Understanding Displaying a DataFrame</h2><p>The table widget doesn&#8217;t know about pandas &#8212; so we need a function that takes any DataFrame and renders it into the Treeview. The pattern is: clear the table, set up the columns, then add every row.</p><pre><code><code>def show_dataframe(self, df):
    # 1. Clear any existing rows
    self.tree.delete(*self.tree.get_children())

    # 2. Set the columns to match the DataFrame
    self.tree["columns"] = list(df.columns)
    for col in df.columns:
        self.tree.heading(col, text=col)
        self.tree.column(col, width=110)

    # 3. Add every row from the DataFrame
    for _, row in df.iterrows():
        self.tree.insert("", "end", values=list(row))
</code></code></pre><p>This one function is reused for <em>both</em> the before and after views. Load the messy data &#8212; call <code>show_dataframe</code>. Clean it &#8212; call <code>show_dataframe</code> again. The table just re-renders whatever DataFrame you hand it.</p><h2>Understanding Loading a File</h2><p>The &#8220;Load CSV&#8221; button opens a file picker with <code>filedialog</code>, then reads the chosen file:</p><pre><code><code>from tkinter import filedialog

def load_csv(self):
    path = filedialog.askopenfilename(
        title="Select a CSV file",
        filetypes=[("CSV files", "*.csv")]
    )
    if not path:          # user clicked Cancel
        return

    self.df = pd.read_csv(path)
    self.show_dataframe(self.df)
    self.log(f"Loaded {path} &#8212; {len(self.df)} rows, {len(self.df.columns)} columns")
</code></code></pre><p><code>filedialog.askopenfilename</code> returns the selected path as a string &#8212; or an empty string if the user cancels, which is why we check <code>if not path</code>. Once loaded, we show the data and log what happened.</p><h2>Understanding the Cleaning Pipeline</h2><p>The cleaning logic is pure pandas &#8212; the same regardless of GUI or CLI. We break it into one method per problem, all called by <code>clean_data()</code>:</p><pre><code><code>def clean_data(self):
    if self.df is None:
        self.log("Load a CSV first!")
        return

    df = self.df

    # --- Strip whitespace from text columns ---
    text_cols = ["customer_name", "product", "category", "status"]
    for col in text_cols:
        df[col] = df[col].astype(str).str.strip()

    # --- Standardize capitalization ---
    df["category"] = df["category"].str.title()   # electronics -&gt; Electronics
    df["status"] = df["status"].str.lower()        # COMPLETED -&gt; completed

    # --- Fix the price column ---
    df["unit_price"] = df["unit_price"].astype(str).str.replace("$", "", regex=False)
    df["unit_price"] = pd.to_numeric(df["unit_price"], errors="coerce")

    # ... (quantity, missing values, total_price) ...

    self.df = df
    self.show_dataframe(self.df)
</code></code></pre><p>The key tools:</p><ul><li><p><code>.str.strip()</code><strong> / </strong><code>.str.title()</code><strong> / </strong><code>.str.lower()</code> &#8212; string operations applied to a whole column via the <code>.str</code> accessor.</p></li><li><p><code>.str.replace("$", "", regex=False)</code> &#8212; strips the dollar sign so the text can become a number.</p></li><li><p><code>pd.to_numeric(..., errors="coerce")</code> &#8212; converts text to numbers; anything unconvertible becomes <code>NaN</code> (missing) instead of crashing.</p></li></ul><h2>Understanding Missing Value Handling</h2><p>After converting types, some cells are missing. For sales data we <em>fill</em> rather than drop &#8212; dropping rows loses real orders.</p><p>Missing <strong>quantity</strong> &#8594; fill with <code>1</code> (an order existed, so at least one item):</p><pre><code><code>df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["quantity"] = df["quantity"].fillna(1).astype(int)
</code></code></pre><p>Missing <strong>unit_price</strong> &#8594; fill with that <em>category&#8217;s average price</em> (a flat number would distort things &#8212; a missing webcam price shouldn&#8217;t become $5):</p><pre><code><code>category_avg = df.groupby("category")["unit_price"].transform("mean")
df["unit_price"] = df["unit_price"].fillna(category_avg).round(2)
</code></code></pre><p><code>groupby(...).transform("mean")</code> computes each category&#8217;s average and aligns it to every row, so each gap is filled with <em>its own category&#8217;s</em> average. &#8220;Handle missing values&#8221; always means <em>think about what the right value is.</em></p><h2>Understanding the Calculated Column</h2><p>Clean, properly-typed data lets you <em>derive</em> new information. Total price is just quantity &#215; unit price &#8212; one line, now that both columns are real numbers:</p><pre><code><code>df["total_price"] = (df["quantity"] * df["unit_price"]).round(2)
</code></code></pre><p>This is the payoff of cleaning. If <code>unit_price</code> were still text with dollar signs, this multiplication would fail outright.</p><h2>Understanding the Status Log</h2><p>A good tool <em>tells the user what it did</em>. We add a scrolling text box at the bottom and append a line for each action:</p><pre><code><code>from tkinter import scrolledtext

# In build_ui:
self.status_log = scrolledtext.ScrolledText(self.root, height=6)

def log(self, message):
    self.status_log.insert(tk.END, message + "\n")
    self.status_log.see(tk.END)   # auto-scroll to the newest line
</code></code></pre><p>Each cleaning step calls <code>self.log(...)</code>. The user gets a running history &#8212; &#8220;stripped whitespace,&#8221; &#8220;filled 3 missing quantities&#8221; &#8212; instead of changes happening invisibly.</p><h2>Understanding Saving the Result</h2><p>The &#8220;Save Clean CSV&#8221; button uses a <em>save</em> dialog and writes the DataFrame out:</p><pre><code><code>def save_csv(self):
    if self.df is None:
        self.log("Nothing to save &#8212; load and clean a file first.")
        return

    path = filedialog.asksaveasfilename(
        defaultextension=".csv",
        filetypes=[("CSV files", "*.csv")]
    )
    if not path:
        return

    self.df.to_csv(path, index=False)
    self.log(f"Saved clean data to {path}")
</code></code></pre><p><code>asksaveasfilename</code> lets the user choose where to save. <code>index=False</code> in <code>to_csv</code> is important &#8212; without it, pandas writes its internal row numbers as a junk extra column.</p><h2>Practical Use Cases</h2><p><strong>1. Cleaning exported reports:</strong></p><pre><code><code>E-commerce, accounting, and CRM exports are notoriously messy &#8212; clean them on arrival.
</code></code></pre><p><strong>2. A reusable cleaning tool:</strong></p><pre><code><code>Build it once, then clean any sales CSV by loading it into the app.
</code></code></pre><p><strong>3. Visual data inspection:</strong></p><pre><code><code>Seeing the table before and after catches problems a script would hide.
</code></code></pre><p><strong>4. Non-technical users:</strong></p><pre><code><code>A GUI means colleagues can clean data without touching code.
</code></code></pre><p><strong>5. Foundation for Day 2:</strong></p><pre><code><code>Tomorrow's analyzer needs clean, typed data &#8212; this app produces exactly that.</code></code></pre><h2>Coming Tomorrow</h2><p>Tomorrow we put the clean data to work. The <strong>Sales Analyzer</strong> takes <code>orders_clean.csv</code> and computes real business insights &#8212; revenue by category, best-selling products, daily trends, average order value &#8212; and generates a formatted summary report.</p><h2>Skeleton and Solution</h2><p>Below you will find both a downloadable skeleton.py file to help you code the project with comment guides and the downloadable solution.py file containing the correct solution.</p><p>Get the code skeleton here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/view/ngPdoU65tK_WmPku2uhgWQ&quot;,&quot;text&quot;:&quot;View Code Skeleton&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/view/ngPdoU65tK_WmPku2uhgWQ"><span>View Code Skeleton</span></a></p><p></p><p>Get the code solution here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/evolution/mvFr1rE85fcLVDOgupikfA&quot;,&quot;text&quot;:&quot;View Code Solution&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/evolution/mvFr1rE85fcLVDOgupikfA"><span>View Code Solution</span></a></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Build an AI Travel Itinerary Planner: Day 3 - Travel Planner Web App]]></title><description><![CDATA[Today we wrap both in a real web app. No more editing variables in a script &#8212; users open a page, type a destination, pick the trip length, list their interests, and click a button.]]></description><link>https://dailypythonprojects.substack.com/p/build-an-ai-travel-itinerary-planner-4f4</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-an-ai-travel-itinerary-planner-4f4</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Fri, 29 May 2026 11:29:57 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e0c332cb-b70a-495a-b722-31098baa8894_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build an <strong>AI Travel Itinerary Planner</strong> that turns a destination into a beautiful, interactive map you can explore and share &#8212; perfect for trip planning!</p><ul><li><p><strong>Day 1:</strong> Interactive Trip Map with Folium</p></li><li><p><strong>Day 2:</strong> AI Itinerary Generator with Gemini</p></li><li><p><strong>Day 3:</strong> Travel Planner Web App <strong>(Today)</strong></p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-19">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p><strong>Welcome to the finale!</strong> We have a map engine and an AI itinerary generator. Today we wrap both in a real <strong>web app</strong>. No more editing variables in a script &#8212; users open a page, type a destination, pick the trip length, list their interests, and click a button. The AI plans the trip and the interactive map renders right on the page, ready to download and share.</p><p>We&#8217;re using <strong>Streamlit</strong>, so the whole app is pure Python &#8212; no HTML, CSS, or JavaScript.</p><h2>Project Task</h2><p>Build a travel planner web app that:</p><ul><li><p>Provides a clean web interface (destination, days, interests inputs)</p></li><li><p>Generates the itinerary with Gemini when the user clicks a button</p></li><li><p>Renders the interactive Folium map directly in the page</p></li><li><p>Shows the itinerary as a readable day-by-day breakdown</p></li><li><p>Lets users download the map as a standalone HTML file</p></li><li><p>Uses session state so the map persists across interactions</p></li><li><p>Shows loading spinners and friendly error messages</p></li><li><p>Reuses the Day 2 AI engine and the Day 1 map engine unchanged</p></li></ul><p>This project gives you hands-on practice with Streamlit, web app layout, session state, embedding interactive components, file downloads, and turning a script into a shareable product &#8212; essential skills for building and publishing data apps.</p><h2>Expected Output</h2><p><strong>Running the web app:</strong></p><pre><code><code>streamlit run travel_app.py</code></code></pre><p>A browser opens automatically at http://localhost:8501 and the user can set the preferences for their trip:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZjUa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZjUa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png 424w, https://substackcdn.com/image/fetch/$s_!ZjUa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png 848w, https://substackcdn.com/image/fetch/$s_!ZjUa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png 1272w, https://substackcdn.com/image/fetch/$s_!ZjUa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZjUa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png" width="1029" height="686" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:686,&quot;width&quot;:1029,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:187398,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199448080?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ZjUa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png 424w, https://substackcdn.com/image/fetch/$s_!ZjUa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png 848w, https://substackcdn.com/image/fetch/$s_!ZjUa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png 1272w, https://substackcdn.com/image/fetch/$s_!ZjUa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb325b6a5-9a08-4804-bb4e-34e5e7b9ddbd_1029x686.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>After they click &#8220;Generate Trip&#8221; they see their itinerary in an interactive map:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WnA2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WnA2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png 424w, https://substackcdn.com/image/fetch/$s_!WnA2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png 848w, https://substackcdn.com/image/fetch/$s_!WnA2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png 1272w, https://substackcdn.com/image/fetch/$s_!WnA2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WnA2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:844556,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199448080?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WnA2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png 424w, https://substackcdn.com/image/fetch/$s_!WnA2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png 848w, https://substackcdn.com/image/fetch/$s_!WnA2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png 1272w, https://substackcdn.com/image/fetch/$s_!WnA2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9e8cdeb-d5e3-4d96-a19a-65fc8cc08cb8_1608x1072.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install streamlit streamlit-folium langchain langchain-google-genai folium
</code></code></pre><p><strong>Add Your Google API Key:</strong></p><p>Open <code>travel_app.py</code> and paste your key into the <code>GOOGLE_API_KEY</code> line near the top:</p><pre><code><code>GOOGLE_API_KEY = "your-key-here"
</code></code></pre><p>(Get a free key at <a href="https://aistudio.google.com/app/apikey">Google AI Studio</a>.)</p><p><strong>Run the app:</strong></p><pre><code><code>streamlit run travel_app.py
</code></code></pre><p>Your browser opens automatically. To stop the app, press <code>Ctrl+C</code> in the terminal.</p><blockquote><p><strong>Security note:</strong> For simplicity, this project keeps the API key as a plain string in the script. That&#8217;s fine for learning and personal use, but if you share your code or push it to GitHub, your key would be exposed. The safer approach is to store the key in a <code>.env</code> file and load it with the <code>python-dotenv</code> package, or &#8212; for a deployed Streamlit app &#8212; use Streamlit&#8217;s built-in <strong>secrets management</strong>. Both keep secrets out of your source code.</p></blockquote><h2>Understanding Streamlit</h2><p>Streamlit turns a Python script into a web app. There&#8217;s no routing, no HTML templates, no frontend code &#8212; you call functions, and Streamlit renders widgets.</p><pre><code><code>import streamlit as st

st.title("My App")                      # a heading
name = st.text_input("Your name")        # an input box
if st.button("Greet"):                   # a button
    st.write(f"Hello, {name}!")          # output text
</code></code></pre><p>The mental model that matters: <strong>Streamlit re-runs your entire script top to bottom every time the user interacts with anything.</strong> Click a button, move a slider, type in a box &#8212; the whole script runs again. That&#8217;s simple and predictable, but it has one big consequence we have to design around (see session state below).</p><h2>Understanding the App Layout</h2><p>We split the screen into a <strong>sidebar</strong> for inputs and a <strong>main area</strong> for the map. <code>st.sidebar</code> puts widgets on the left:</p><pre><code><code>import streamlit as st

st.set_page_config(page_title="AI Travel Planner", page_icon="&#9992;&#65039;", layout="wide")

st.title("&#9992;&#65039; AI Travel Itinerary Planner")
st.caption("Describe your trip and get an interactive map in seconds.")

# --- Sidebar inputs ---
with st.sidebar:
    st.header("Plan your trip")
    destination = st.text_input("Destination", value="Rome")
    days = st.slider("Trip length (days)", min_value=1, max_value=7, value=3)
    interests = st.text_area("Your interests", value="food, history, art")
    generate = st.button("Generate Trip", type="primary")
</code></code></pre><p><code>layout="wide"</code> gives the map room to breathe. <code>st.slider</code> is a natural fit for trip length &#8212; bounded, no invalid input possible. <code>type="primary"</code> makes the button stand out.</p><h2>Understanding the Download Button</h2><p>A shareable map should be downloadable. Streamlit&#8217;s <code>st.download_button</code> lets users save a file &#8212; and Folium can render a map to an HTML string in memory with <code>get_root().render()</code>:</p><pre><code><code>def map_to_html(trip_map):
    """Render the Folium map to a standalone HTML string."""
    return trip_map.get_root().render()

st.download_button(
    label="&#128229; Download Map as HTML",
    data=map_to_html(st.session_state.trip_map),
    file_name="my_trip.html",
    mime="text/html",
)
</code></code></pre><p>No temp files, no disk writes &#8212; the HTML is generated in memory and streamed straight to the user&#8217;s browser as a download. The file they get is the same standalone map from Days 1 and 2.</p><h2>Understanding the Itinerary Display</h2><p>Below the map, we show the trip as readable text. We group stops by day and use an emoji per category for a friendly touch:</p><pre><code><code>CATEGORY_EMOJI = {
    "food": "&#127860;",
    "sightseeing": "&#128247;",
    "hotel": "&#128719;&#65039;",
    "activity": "&#11088;",
    "transport": "&#9992;&#65039;",
}

def show_itinerary(itinerary):
    st.subheader("Your Itinerary")
    stops = itinerary_to_stops(itinerary)

    for day in get_days(stops):
        st.markdown(f"**Day {day}**")
        for stop in get_stops_for_day(stops, day):
            emoji = CATEGORY_EMOJI.get(stop["category"], "&#128205;")
            st.markdown(f"{emoji} **{stop['name']}** &#8212; {stop['description']}")
</code></code></pre><p><code>st.markdown</code> renders bold text and emoji cleanly. The same <code>get_days</code> and <code>get_stops_for_day</code> helpers from Day 1 organize the output &#8212; more reuse of code we already trust.</p><h2>Understanding the Full Picture</h2><p>Look at how the three days fit together:</p><ul><li><p><strong>Day 1</strong> built <code>build_trip_map()</code> &#8212; takes stop dicts, returns an interactive map.</p></li><li><p><strong>Day 2</strong> built <code>generate_itinerary()</code> and <code>itinerary_to_stops()</code> &#8212; turns plain English into stop dicts via Gemini.</p></li><li><p><strong>Day 3</strong> added <em>only</em> the web layer &#8212; Streamlit widgets, session state, <code>st_folium</code>, a download button.</p></li></ul><p>The map engine and the AI engine were never rewritten. Day 3&#8217;s job was purely to wrap them in an interface. That&#8217;s the mark of well-structured code: <strong>each layer has one job, and new layers build on top without disturbing what&#8217;s below.</strong></p><h2>What You&#8217;ve Accomplished This Week</h2><p>&#127881; <strong>Congratulations!</strong> You&#8217;ve built a complete <strong>AI-powered travel planning web app</strong>!</p><ul><li><p><strong>Day 1:</strong> Interactive map engine with Folium</p></li><li><p><strong>Day 2:</strong> AI itinerary generation with Gemini + LangChain v1</p></li><li><p><strong>Day 3:</strong> A real web app that ties it together</p></li></ul><p><strong>You now have:</strong></p><p>&#9989; <strong>Interactive maps</strong> &#8212; color-coded, route-drawn, popup-rich &#9989; <strong>AI itinerary generation</strong> &#8212; plain English in, structured trip out &#9989; <strong>A real web interface</strong> &#8212; no script editing required &#9989; <strong>Downloadable, shareable maps</strong> &#8212; standalone HTML &#9989; <strong>Clean architecture</strong> &#8212; layered engine, AI, and UI</p><p><strong>Real-world applications:</strong></p><ul><li><p><strong>Personal trip planning</strong> &#8212; your own travel tool</p></li><li><p><strong>Travel blogs</strong> &#8212; let readers generate and explore routes</p></li><li><p><strong>Travel agencies</strong> &#8212; rapid first-draft itineraries for clients</p></li><li><p><strong>Portfolio project</strong> &#8212; AI + maps + web app in one</p></li><li><p><strong>A product starting point</strong> &#8212; add accounts, saved trips, bookings</p></li></ul><p><strong>Next steps:</strong></p><ul><li><p>Deploy it free on Streamlit Community Cloud so anyone can use it</p></li><li><p>Let users edit or reorder stops before downloading</p></li><li><p>Add hotel and restaurant suggestions with booking links</p></li><li><p>Support multi-city trips</p></li><li><p>Add trip cost estimates</p></li></ul><p>You&#8217;ve built the foundation for a <strong>real travel-tech product</strong>! &#128640;</p><h2>View Code Evolution</h2><p>Compare today&#8217;s web app with the Day 2 generator and the Day 1 map engine &#8212; and see how a clean, layered design lets you add a whole new interface without touching the code beneath it.</p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/build-an-ai-travel-itinerary-planner-4f4">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build an AI Travel Itinerary Planner: Day 2 - AI Itinerary Generator with Gemini ]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/build-an-ai-travel-itinerary-planner-eb0</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-an-ai-travel-itinerary-planner-eb0</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Thu, 28 May 2026 11:15:51 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ea6b78da-b193-4028-b70f-20387dbd8eb2_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build an <strong>AI Travel Itinerary Planner</strong> that turns a destination into a beautiful, interactive map you can explore and share &#8212; perfect for trip planning!</p><ul><li><p><strong>Day 1:</strong> Interactive Trip Map with Folium</p></li><li><p><strong>Day 2:</strong> AI Itinerary Generator with Gemini <strong>(Today)</strong></p></li><li><p><strong>Day 3:</strong> Travel Planner Web App</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-19">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>Yesterday we built the map engine and hand-fed it a Lisbon itinerary. Today the <strong>AI writes the itinerary for us</strong>. You describe a trip in plain English &#8212; &#8220;4 days in Rome, I love food and ancient history&#8221; &#8212; and Gemini, through LangChain v1, returns a fully structured itinerary with coordinates baked in. That output drops straight into yesterday&#8217;s map code, unchanged.</p><p>This is the payoff of Day 1&#8217;s design: because we built a stable data shape first, the AI just has to fill it.</p><h2>Project Task</h2><p>Build an AI itinerary generator that:</p><ul><li><p>Takes a plain-English trip request (destination, days, interests)</p></li><li><p>Uses LangChain v1 + Gemini to generate a structured itinerary</p></li><li><p>Returns each stop with name, day, lat, lon, category, and description</p></li><li><p>Uses Pydantic models so the AI output is validated and type-safe</p></li><li><p>Uses <code>with_structured_output()</code> &#8212; no manual JSON parsing</p></li><li><p>Feeds the AI itinerary into the Day 1 Folium map engine</p></li><li><p>Handles API errors and missing keys gracefully</p></li><li><p>Saves the finished map as a shareable HTML file</p></li></ul><p>This project gives you hands-on practice with LangChain v1, the Gemini API, structured AI output, Pydantic schema validation, and connecting an AI layer to a visualization layer &#8212; essential skills for AI-powered applications.</p><h2>Expected Output</h2><p><strong>Running the AI generator:</strong></p><pre><code><code>python ai_trip_map.py
</code></code></pre><p><strong>Console Output:</strong><br>The user can define their trip preferences in the terminal and the program will print out an itinerary:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aeBV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aeBV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png 424w, https://substackcdn.com/image/fetch/$s_!aeBV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png 848w, https://substackcdn.com/image/fetch/$s_!aeBV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png 1272w, https://substackcdn.com/image/fetch/$s_!aeBV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aeBV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1304776,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199445384?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aeBV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png 424w, https://substackcdn.com/image/fetch/$s_!aeBV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png 848w, https://substackcdn.com/image/fetch/$s_!aeBV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png 1272w, https://substackcdn.com/image/fetch/$s_!aeBV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c96742e-bd0a-44c3-8662-d4d3568db71d_2484x1656.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>After that runs an HTML file will be created which can be opened in the browser. It will contain the itinerary with interactive map pins:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XJiQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XJiQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png 424w, https://substackcdn.com/image/fetch/$s_!XJiQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png 848w, https://substackcdn.com/image/fetch/$s_!XJiQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png 1272w, https://substackcdn.com/image/fetch/$s_!XJiQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XJiQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png" width="1413" height="942" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:942,&quot;width&quot;:1413,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1389962,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199445384?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XJiQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png 424w, https://substackcdn.com/image/fetch/$s_!XJiQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png 848w, https://substackcdn.com/image/fetch/$s_!XJiQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png 1272w, https://substackcdn.com/image/fetch/$s_!XJiQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f4c8e7b-7538-490b-ada1-1d0662010ad9_1413x942.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p></p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install langchain langchain-google-genai folium
</code></code></pre><p><strong>Get Your Google API Key:</strong></p><ol><li><p>Go to <a href="https://aistudio.google.com/app/apikey">Google AI Studio</a></p></li><li><p>Click &#8220;Create API Key&#8221; and copy it</p></li><li><p>Assign the key to the GOOGLE_API_KEY variable as a string in the code.</p></li></ol><p><strong>Run it:</strong></p><pre><code><code>python ai_trip_map.py</code></code></pre><h2>Understanding Structured Output</h2><p>The hardest part of using an LLM in real code is that it returns <em>text</em>. Free-form text is unreliable &#8212; you&#8217;d be writing fragile parsers and praying the model uses the right quote style.</p><p>LangChain&#8217;s <code>with_structured_output()</code> solves this. You define the shape you want as a Pydantic model, and the AI is <em>constrained</em> to return exactly that shape &#8212; already parsed into Python objects:</p><pre><code><code>from pydantic import BaseModel
from langchain_google_genai import ChatGoogleGenerativeAI

class Person(BaseModel):
    name: str
    age: int

llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
structured_llm = llm.with_structured_output(Person)

result = structured_llm.invoke("Tell me about a person named Alice, age 30")
# result is a Person object: Person(name='Alice', age=30)
</code></code></pre><p>No JSON parsing. No string cleanup. You get a typed object, and if the AI returns something malformed, Pydantic raises a clear error instead of silently corrupting your data.</p><h2>Understanding the Pydantic Schema</h2><p>We need the AI to return the <em>exact</em> data shape our Day 1 map expects. So we describe it as Pydantic models &#8212; one for a single stop, one for the whole itinerary:</p><pre><code><code>from pydantic import BaseModel, Field

class Stop(BaseModel):
    """A single stop on the trip."""
    name: str = Field(description="Name of the place")
    day: int = Field(description="Which day of the trip this stop is on")
    lat: float = Field(description="Latitude coordinate")
    lon: float = Field(description="Longitude coordinate")
    category: str = Field(description="One of: food, sightseeing, hotel, activity, transport")
    description: str = Field(description="One-sentence description of the stop")

class Itinerary(BaseModel):
    """A complete multi-day travel itinerary."""
    trip_title: str = Field(description="A short title for the trip")
    stops: list[Stop] = Field(description="All stops across all days, in visiting order")
</code></code></pre><p><strong>Why the </strong><code>Field(description=...)</code><strong> matters:</strong> those descriptions aren&#8217;t just documentation. LangChain sends them to the model as part of the schema. They&#8217;re how you tell the AI &#8220;category must be one of these five values&#8221; or &#8220;give me one sentence, not a paragraph.&#8221; Good field descriptions are effectively prompt engineering.</p><h2>Understanding the LangChain v1 + Gemini Setup</h2><p>LangChain v1 keeps the model setup clean. You create the chat model, then attach the schema:</p><pre><code><code>import os
from langchain_google_genai import ChatGoogleGenerativeAI

def build_ai():
    llm = ChatGoogleGenerativeAI(
        model="gemini-2.5-flash",
        google_api_key=os.getenv("GOOGLE_API_KEY"),
        temperature=0.7,  # a little creativity for varied suggestions
    )
    # Constrain every response to the Itinerary schema
    return llm.with_structured_output(Itinerary)
</code></code></pre><p><code>temperature=0.7</code> is a deliberate choice: trip planning benefits from variety, so we don&#8217;t want the near-deterministic <code>0</code> you&#8217;d use for data extraction. The structured-output constraint still guarantees the <em>shape</em> &#8212; temperature only affects the <em>content</em>.</p><h2>Understanding the Prompt</h2><p>Even with structured output, the prompt still matters &#8212; it&#8217;s where you give the AI its instructions and quality bar:</p><pre><code><code>def build_prompt(destination, days, interests):
    return f"""You are an expert travel planner.

Create a {days}-day travel itinerary for {destination}.
The traveler is interested in: {interests}.

Requirements:
- Plan exactly 3 stops per day.
- Spread stops geographically so each day is walkable where possible.
- Include a mix of categories, with at least one food stop per day.
- Use real, well-known places with accurate latitude and longitude.
- Keep each description to a single vivid sentence.
- Order stops within each day in a sensible visiting sequence.
"""
</code></code></pre><p>Notice we ask for <strong>accurate latitude and longitude</strong> directly. Gemini knows the coordinates of famous landmarks well, so for a travel planner this is reliable enough &#8212; and it keeps the project to a single API call with no separate geocoding service.</p><h2>Understanding the AI-to-Map Handoff</h2><p>Here&#8217;s where Day 1&#8217;s design pays off. The AI returns an <code>Itinerary</code> object full of <code>Stop</code> objects. Our map engine expects a list of plain dicts. One small conversion bridges them:</p><pre><code><code>def itinerary_to_stops(itinerary):
    """Convert the AI's Pydantic Itinerary into the dict list the map expects."""
    return [
        {
            "name": stop.name,
            "day": stop.day,
            "lat": stop.lat,
            "lon": stop.lon,
            "category": stop.category,
            "description": stop.description,
        }
        for stop in itinerary.stops
    ]
</code></code></pre><p>After this, <strong>every map function from Day 1 works untouched</strong> &#8212; <code>build_trip_map</code>, <code>add_day_to_map</code>, <code>fit_map_to_stops</code>, all of it. The AI is just a new data source plugged into a finished engine.</p><h2>Understanding Error Handling</h2><p>AI calls fail in ways normal code doesn&#8217;t &#8212; missing keys, rate limits, network drops. A travel app shouldn&#8217;t crash on any of them:</p><pre><code><code>def generate_itinerary(ai, destination, days, interests):
    if not os.getenv("GOOGLE_API_KEY"):
        print("&#10007; No GOOGLE_API_KEY found. Set it as an environment variable.")
        return None

    prompt = build_prompt(destination, days, interests)

    try:
        return ai.invoke(prompt)
    except Exception as e:
        print(f"&#10007; Itinerary generation failed: {e}")
        return None
</code></code></pre><p>The check for the missing key happens <em>before</em> the call &#8212; a fast, friendly failure. The <code>try/except</code> catches everything else and reports it instead of dumping a stack trace.</p><h2>Understanding Why Pydantic Validation Helps</h2>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/build-an-ai-travel-itinerary-planner-eb0">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build an AI Travel Itinerary Planner: Day 1 - Interactive Trip Map with Folium]]></title><description><![CDATA[Build an interactive HTML map with color-coded day markers, drawn routes, detailed popups, and professional touches like a minimap and fullscreen.]]></description><link>https://dailypythonprojects.substack.com/p/build-an-ai-travel-itinerary-planner</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-an-ai-travel-itinerary-planner</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Wed, 27 May 2026 10:42:27 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/66278b0f-7610-4f33-8ee4-00d21c9fcadd_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build an <strong>AI Travel Itinerary Planner</strong> that turns a destination into a beautiful, interactive map you can explore and share &#8212; perfect for trip planning!</p><p><strong>Why build this?</strong> Because planning a trip means juggling places, routes, and notes across a dozen browser tabs. You&#8217;ll build a tool that puts everything on one interactive map &#8212; and by Day 3, an AI generates the whole itinerary for you.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong>What you&#8217;ll learn:</strong> Interactive map creation, geographic data visualization, AI-powered content generation, and building shareable web apps &#8212; essential skills for location-based applications.</p><p><strong>Why users love this:</strong> Type a city, get a gorgeous map with every stop pinned, routes drawn, and tips in every popup. It feels like a real travel product.</p><ul><li><p><strong>Day 1:</strong> Interactive Trip Map with Folium <strong>(Today)</strong></p></li><li><p><strong>Day 2:</strong> AI Itinerary Generator with Gemini</p></li><li><p><strong>Day 3:</strong> Travel Planner Web App</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-19">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>We&#8217;re starting with the <strong>map engine</strong> &#8212; the visual heart of the whole project. <strong>No AI yet.</strong> Today you master Folium: take a structured trip itinerary and turn it into a rich, interactive HTML map with color-coded day markers, drawn routes, detailed popups, and professional touches like a minimap and fullscreen mode.</p><p>By the end, you&#8217;ll have a real interactive map you can open in any browser and share. On Day 2, the AI will <em>generate</em> the data you&#8217;re hand-feeding it today.</p><h2>Project Task</h2><p>Build an interactive trip map generator that:</p><ul><li><p>Takes a structured multi-day itinerary (list of stops with name, day, coordinates, description, category)</p></li><li><p>Plots every stop as a marker on a Folium map</p></li><li><p>Color-codes markers by day (Day 1 = blue, Day 2 = green, etc.)</p></li><li><p>Uses category-based icons (food, sightseeing, hotel, activity)</p></li><li><p>Draws a route line connecting stops in order, per day</p></li><li><p>Rich HTML popups with stop name, description, and category</p></li><li><p>Auto-centers and auto-zooms to fit all stops</p></li><li><p>Adds a minimap, fullscreen button, and layer control</p></li><li><p>Saves the result as a standalone, shareable HTML file</p></li></ul><p>This project gives you hands-on practice with Folium, interactive maps, geographic visualization, HTML generation, and structured data handling &#8212; essential skills for location-based applications.</p><h2>Expected Output</h2><p><strong>Running the map generator:</strong></p><pre><code><code>python trip_map.py
</code></code></pre><p>Running the script will generate an HTML file which can be opened in the browser:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UNSM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UNSM!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif 424w, https://substackcdn.com/image/fetch/$s_!UNSM!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif 848w, https://substackcdn.com/image/fetch/$s_!UNSM!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif 1272w, https://substackcdn.com/image/fetch/$s_!UNSM!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UNSM!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif" width="500" height="288" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:288,&quot;width&quot;:500,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9772464,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/199188254?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UNSM!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif 424w, https://substackcdn.com/image/fetch/$s_!UNSM!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif 848w, https://substackcdn.com/image/fetch/$s_!UNSM!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif 1272w, https://substackcdn.com/image/fetch/$s_!UNSM!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d89f8ec-4fef-41d5-8e75-eb9447032bde_500x288.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><br></p><h2>Setup Instructions</h2><p><strong>Install Required Package:</strong></p><pre><code><code>pip install folium</code></code></pre><p>That&#8217;s it &#8212; one dependency. Folium generates the map as HTML; no server needed.</p><p><strong>Run it:</strong></p><pre><code><code>python trip_map.py
</code></code></pre><p>Then open <code>trip_map.html</code> in any browser.</p><h2>Understanding the Itinerary Data Structure</h2><p>Before drawing anything, we need a clean data structure. Each stop is a dictionary, and the trip is a list of them:</p><pre><code><code>itinerary = [
    {
        "name": "Time Out Market",
        "day": 1,
        "lat": 38.7067,
        "lon": -9.1459,
        "category": "food",
        "description": "Historic food hall with dozens of local vendors."
    },
    {
        "name": "S&#227;o Jorge Castle",
        "day": 1,
        "lat": 38.7139,
        "lon": -9.1335,
        "category": "sightseeing",
        "description": "Moorish castle with panoramic views over the city."
    },
    # ... more stops
]
</code></code></pre><p><strong>Why this structure?</strong></p><p>It is flat, predictable, and easy to generate. On Day 2, Gemini will return <em>exactly this shape</em> as JSON &#8212; so the map code you write today works unchanged when the AI takes over. That is the whole point of starting with the map: you build a stable target, then plug the AI into it.</p><h2>Understanding Folium Basics</h2><p>Folium is a Python wrapper around Leaflet.js. You build a map in Python, and it writes the HTML/JavaScript for you.</p><pre><code><code>import folium

# Create a map centered on a location
m = folium.Map(location=[38.7223, -9.1393], zoom_start=13)

# Add a marker
folium.Marker(
    location=[38.7067, -9.1459],
    popup="Time Out Market",
    tooltip="Click for details"
).add_to(m)

# Save to HTML
m.save("map.html")
</code></code></pre><p>Three core ideas:</p><ul><li><p><strong>The map object</strong> (<code>folium.Map</code>) is the canvas &#8212; everything gets added to it.</p></li><li><p><strong>Elements</strong> (markers, lines, layers) are created, then attached with <code>.add_to(m)</code>.</p></li><li><p><code>.save()</code> writes a complete, standalone HTML file &#8212; no server, no dependencies for the viewer.</p></li></ul><h2>Understanding Color-Coded Day Markers</h2><p>To make a multi-day trip readable, each day gets its own color. Folium markers accept an <code>Icon</code> with a <code>color</code> and an <code>icon</code> glyph:</p><pre><code><code>DAY_COLORS = ["blue", "green", "purple", "orange", "red", "darkblue"]

def get_day_color(day_number):
    # Day 1 -&gt; index 0, wraps around for long trips
    return DAY_COLORS[(day_number - 1) % len(DAY_COLORS)]

folium.Marker(
    location=[stop["lat"], stop["lon"]],
    icon=folium.Icon(color=get_day_color(stop["day"]), icon="cutlery", prefix="fa")
).add_to(m)
</code></code></pre><p><strong>Why </strong><code>prefix="fa"</code><strong>?</strong> It tells Folium to use Font Awesome icons, which gives you a much larger glyph library (cutlery, camera, bed, etc.) than the default set.</p><h2>Understanding Category Icons</h2><p>Color tells you <em>which day</em>. The icon glyph tells you <em>what kind of stop</em> it is. A simple dictionary maps categories to Font Awesome icons:</p><pre><code><code>CATEGORY_ICONS = {
    "food": "cutlery",
    "sightseeing": "camera",
    "hotel": "bed",
    "activity": "star",
    "transport": "plane",
}

def get_category_icon(category):
    # Fallback to a generic marker if the category is unknown
    return CATEGORY_ICONS.get(category, "info-sign")
</code></code></pre><p>Now a blue cutlery marker reads instantly as &#8220;Day 1 food stop.&#8221; This visual encoding is what separates a real map from a pile of identical pins.</p><h2>Understanding Rich HTML Popups</h2><p>A popup can be plain text &#8212; or it can be styled HTML. Folium accepts an HTML string, so you can build a proper info card:</p><pre><code><code>def build_popup_html(stop):
    return f"""
    &lt;div style="width: 220px; font-family: sans-serif;"&gt;
        &lt;h4 style="margin: 0 0 6px 0;"&gt;{stop['name']}&lt;/h4&gt;
        &lt;hr style="margin: 4px 0;"&gt;
        &lt;p style="margin: 4px 0;"&gt;&lt;b&gt;{stop['category'].title()}&lt;/b&gt; &#183; Day {stop['day']}&lt;/p&gt;
        &lt;p style="margin: 4px 0; color: #444;"&gt;{stop['description']}&lt;/p&gt;
    &lt;/div&gt;
    """

popup = folium.Popup(build_popup_html(stop), max_width=250)
folium.Marker(location=[stop["lat"], stop["lon"]], popup=popup).add_to(m)
</code></code></pre><p><strong>Key detail:</strong> wrap the HTML in <code>folium.Popup(..., max_width=250)</code>. Without <code>max_width</code>, long descriptions render as one ugly unwrapped line. The <code>&lt;div&gt;</code> with a fixed width keeps every popup tidy.</p><h2>Understanding Route Lines with PolyLine</h2><p>To connect the stops of a day in order, use <code>folium.PolyLine</code> &#8212; it draws a line through a list of coordinates:</p><pre><code><code>def draw_day_route(day_stops, color, map_obj):
    # day_stops must already be in visiting order
    coordinates = [[stop["lat"], stop["lon"]] for stop in day_stops]

    folium.PolyLine(
        locations=coordinates,
        color=color,
        weight=4,
        opacity=0.7,
        dash_array="8"  # dashed line looks like a travel route
    ).add_to(map_obj)
</code></code></pre><p>We draw <strong>one PolyLine per day</strong>, using that day&#8217;s color. The result: three colored, dashed routes that show the flow of each day, not one tangled line across the whole trip.</p><h2>Understanding Auto-Centering and Auto-Zoom</h2><p>Hardcoding <code>location</code> and <code>zoom_start</code> breaks the moment the trip changes. Instead, let the data decide the view with <code>fit_bounds</code>:</p><pre><code><code>def fit_map_to_stops(map_obj, itinerary):
    lats = [stop["lat"] for stop in itinerary]
    lons = [stop["lon"] for stop in itinerary]

    # Bounding box: [[south, west], [north, east]]
    south_west = [min(lats), min(lons)]
    north_east = [max(lats), max(lons)]

    map_obj.fit_bounds([south_west, north_east])
</code></code></pre><p><code>fit_bounds</code> computes the zoom and center so every stop is visible with a little padding. Now the map works for a walkable city or a sprawling road trip without any manual tuning &#8212; which matters on Day 2, when the AI generates unpredictable destinations.</p><h2>Understanding LayerControl and FeatureGroups</h2><p>To toggle days on and off, each day goes into its own <code>FeatureGroup</code>. A <code>LayerControl</code> then renders the checkboxes:</p><pre><code><code>day_group = folium.FeatureGroup(name="Day 1 &#8212; Historic Center")

# add that day's markers and route to the group instead of the map
marker.add_to(day_group)

day_group.add_to(map_obj)

# add this LAST, after all groups exist
folium.LayerControl(collapsed=False).add_to(map_obj)
</code></code></pre><p><strong>Order matters:</strong> add <code>LayerControl</code> <em>after</em> every <code>FeatureGroup</code>, or some layers won&#8217;t appear in the control.</p><h2>Understanding Map Plugins</h2><p>Folium ships plugins that add real polish in one line each:</p><pre><code><code>from folium.plugins import MiniMap, Fullscreen

# Small overview map in the corner
MiniMap(toggle_display=True).add_to(m)

# Fullscreen button
Fullscreen().add_to(m)
</code></code></pre><p>The <code>MiniMap</code> helps users keep their bearings when zoomed in; <code>Fullscreen</code> makes the shared HTML feel like a proper app. Small additions, big difference in perceived quality.</p><h2>Practical Use Cases</h2><p><strong>1. Personal trip planning:</strong></p><pre><code><code>Hand-build your itinerary, get one map with every stop, route, and note.
</code></code></pre><p><strong>2. Sharing plans with travel companions:</strong></p><pre><code><code>Send the HTML file &#8212; they open it, no install, fully interactive.
</code></code></pre><p><strong>3. Travel blogs and guides:</strong></p><pre><code><code>Embed the map in a post so readers can explore the route themselves.
</code></code></pre><p><strong>4. Tour and event planning:</strong></p><pre><code><code>Color-code by day or group, drop notes in popups, share one link.
</code></code></pre><p><strong>5. Foundation for Day 2:</strong></p><pre><code><code>The exact data shape Gemini will return &#8212; the map is ready before the AI exists.
</code></code></pre><h2>Coming Tomorrow</h2><p>Tomorrow we add the <strong>AI</strong>. Instead of hand-writing the itinerary, you&#8217;ll describe a trip in plain English (&#8221;3 days in Lisbon, I love food and history&#8221;) and Gemini &#8212; via LangChain v1 &#8212; generates the full structured itinerary, coordinates included, ready to drop straight into today&#8217;s map engine.</p><h2>Skeleton and Solution</h2><p>Below you will find both a downloadable skeleton.py file to help you code the project with comment guides and the downloadable solution.py file containing the correct solution.</p><p>Get the code skeleton here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/view/AxDgTc2XGQHO_yxqM944Iw&quot;,&quot;text&quot;:&quot;View Code Skeleton&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/view/AxDgTc2XGQHO_yxqM944Iw"><span>View Code Skeleton</span></a></p><p></p><p>Get the code solution here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/evolution/cpotUXrFcCxJaE4DMYSIzQ&quot;,&quot;text&quot;:&quot;View Code Solution&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/evolution/cpotUXrFcCxJaE4DMYSIzQ"><span>View Code Solution</span></a></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Build a Workout Tracker Suite: Day 3- Complete Workout Dashboard ]]></title><description><![CDATA[Today we&#8217;re creating a complete training dashboard with templates, 1RM calculator, strength standards, and session timers!]]></description><link>https://dailypythonprojects.substack.com/p/build-a-workout-tracker-suite-day-c58</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-workout-tracker-suite-day-c58</guid><pubDate>Thu, 21 May 2026 11:03:49 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4e76f4bd-9fc1-48be-8771-061c0f2b3b41_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>Workout Tracker Suite</strong> that helps you log lifts, visualize progress, and optimize your training &#8212; perfect for anyone serious about strength training!</p><ul><li><p><strong>Day 1:</strong> Barbell Plate Calculator (Tkinter GUI)</p></li><li><p><strong>Day 2:</strong> Workout Logger with Live Charts</p></li><li><p><strong>Day 3:</strong> Complete Workout Dashboard <strong>(Today)</strong></p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-18">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p><strong>Welcome to the finale!</strong> We&#8217;ve built a plate calculator and workout logger. Today we&#8217;re creating a <strong>complete training dashboard</strong> with templates, 1RM calculator, strength standards, and session timers!</p><p>You&#8217;ll have a professional-grade workout tracking system that rivals commercial fitness apps!</p><h2>Project Task</h2><p>Create a complete workout dashboard with:</p><ul><li><p>Workout templates (Push/Pull/Legs, Upper/Lower, Full Body)</p></li><li><p>1RM (one-rep max) calculator with multiple formulas</p></li><li><p>Strength standards comparison (beginner/intermediate/advanced)</p></li><li><p>Session timer with rest period alerts</p></li><li><p>Progressive overload recommendations</p></li><li><p>Volume tracking with periodization suggestions</p></li><li><p>Export workout reports as text files</p></li><li><p>Enhanced charts with volume and intensity trends</p></li><li><p>All Day 1 &amp; 2 features integrated</p></li></ul><p>This project gives you hands-on practice with advanced GUI design, mathematical formulas, timer functionality, template systems, data analysis, and building production-ready fitness applications &#8212; essential skills for complete software solutions!</p><h2>Expected Output</h2><p>We have gone one step further in this version of the GUI and added tabs. This is a major milestone because it demonstrates one of the most popular techniques to organize a GUI when we are adding more features in the app. With tabs we can organize the various functionalities of the apps as you can see below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!chk2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!chk2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png 424w, https://substackcdn.com/image/fetch/$s_!chk2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png 848w, https://substackcdn.com/image/fetch/$s_!chk2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png 1272w, https://substackcdn.com/image/fetch/$s_!chk2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!chk2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:870408,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/198272856?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!chk2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png 424w, https://substackcdn.com/image/fetch/$s_!chk2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png 848w, https://substackcdn.com/image/fetch/$s_!chk2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png 1272w, https://substackcdn.com/image/fetch/$s_!chk2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12d9639f-c236-4921-888f-5f4cc0e99fea_2496x1664.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For example, we have now moved the weight progress to a new &#8220;Analytics&#8221; tab:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aH4b!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aH4b!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png 424w, https://substackcdn.com/image/fetch/$s_!aH4b!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png 848w, https://substackcdn.com/image/fetch/$s_!aH4b!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png 1272w, https://substackcdn.com/image/fetch/$s_!aH4b!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aH4b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1065833,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/198272856?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aH4b!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png 424w, https://substackcdn.com/image/fetch/$s_!aH4b!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png 848w, https://substackcdn.com/image/fetch/$s_!aH4b!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png 1272w, https://substackcdn.com/image/fetch/$s_!aH4b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f244f7-a00f-473c-9807-8cd15948e6a6_2496x1664.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And we have also added other tabs as it can be seen in the screenshots. <br></p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install matplotlib pandas
</code></code></pre><p><strong>Run the dashboard:</strong></p><pre><code><code>python workout_dashboard.py
</code></code></pre><p><strong>All features ready to use:</strong></p><ul><li><p>No additional setup needed</p></li><li><p>Data persists in CSV files</p></li><li><p>Templates saved as JSON</p></li><li><p>Reports export as TXT files</p></li></ul><h2>Understanding 1RM Calculations</h2><p><strong>What is 1RM?</strong></p><p>Your <strong>one-rep max</strong> - the maximum weight you can lift for exactly one rep.</p><p><strong>Why calculate it?</strong></p><ul><li><p>Design training programs (% of 1RM)</p></li><li><p>Track true strength gains</p></li><li><p>Compare against standards</p></li><li><p>Set realistic goals</p></li></ul><p><strong>Common formulas:</strong></p><p><strong>1. Epley Formula:</strong></p><pre><code><code>1RM = weight &#215; (1 + reps/30)

# Example: 225 lbs &#215; 8 reps
1RM = 225 &#215; (1 + 8/30)
1RM = 225 &#215; 1.267
1RM = 285 lbs
</code></code></pre><p><strong>2. Brzycki Formula:</strong></p><pre><code><code>1RM = weight &#215; (36 / (37 - reps))

# Example: 225 lbs &#215; 8 reps
1RM = 225 &#215; (36 / (37 - 8))
1RM = 225 &#215; (36 / 29)
1RM = 279 lbs
</code></code></pre><p><strong>3. Lander Formula:</strong></p><pre><code><code>1RM = (100 &#215; weight) / (101.3 - 2.67123 &#215; reps)

# Example: 225 lbs &#215; 8 reps
1RM = (100 &#215; 225) / (101.3 - 2.67123 &#215; 8)
1RM = 22500 / 79.93
1RM = 281 lbs
</code></code></pre><p><strong>Our implementation:</strong></p><pre><code><code>def calculate_1rm(weight, reps):
    # Epley
    epley = weight * (1 + reps/30)
    
    # Brzycki
    brzycki = weight * (36 / (37 - reps))
    
    # Lander
    lander = (100 * weight) / (101.3 - 2.67123 * reps)
    
    # Lombardi
    lombardi = weight * (reps ** 0.10)
    
    # Average
    average = (epley + brzycki + lander + lombardi) / 4
    
    return {
        'epley': round(epley, 1),
        'brzycki': round(brzycki, 1),
        'lander': round(lander, 1),
        'lombardi': round(lombardi, 1),
        'average': round(average, 1)
    }
</code></code></pre><h2>Understanding Strength Standards</h2><p><strong>What are strength standards?</strong></p><p>Benchmarks for comparing your lifts to typical progression levels.</p><p><strong>Standard categories:</strong></p><pre><code><code>STRENGTH_STANDARDS = {
    'Bench Press': {
        'Beginner': 0.75,      # 0.75&#215; bodyweight
        'Novice': 1.0,         # 1.0&#215; bodyweight
        'Intermediate': 1.25,  # 1.25&#215; bodyweight
        'Advanced': 1.5,       # 1.5&#215; bodyweight
        'Elite': 1.75,         # 1.75&#215; bodyweight
    },
    'Squat': {
        'Beginner': 1.0,
        'Novice': 1.5,
        'Intermediate': 2.0,
        'Advanced': 2.5,
        'Elite': 3.0,
    },
    'Deadlift': {
        'Beginner': 1.25,
        'Novice': 1.75,
        'Intermediate': 2.25,
        'Advanced': 2.75,
        'Elite': 3.25,
    }
}
</code></code></pre><p></p><h2>Understanding Workout Templates</h2><p><strong>What are templates?</strong></p><p>Pre-designed workout programs with exercises, sets, reps, and rest times.</p><p><strong>Common splits:</strong></p><p><strong>Push/Pull/Legs (PPL):</strong></p><pre><code><code>TEMPLATES = {
    'Push': [
        {'exercise': 'Bench Press', 'sets': 4, 'reps': 8, 'rest': 180},
        {'exercise': 'Overhead Press', 'sets': 3, 'reps': 10, 'rest': 120},
        {'exercise': 'Incline DB Press', 'sets': 3, 'reps': 12, 'rest': 90},
        {'exercise': 'Lateral Raises', 'sets': 3, 'reps': 15, 'rest': 60},
        {'exercise': 'Tricep Extensions', 'sets': 3, 'reps': 12, 'rest': 60},
    ],
    'Pull': [
        {'exercise': 'Deadlift', 'sets': 3, 'reps': 5, 'rest': 180},
        {'exercise': 'Pull-ups', 'sets': 3, 'reps': 10, 'rest': 120},
        {'exercise': 'Barbell Row', 'sets': 4, 'reps': 8, 'rest': 120},
        {'exercise': 'Face Pulls', 'sets': 3, 'reps': 15, 'rest': 60},
        {'exercise': 'Bicep Curls', 'sets': 3, 'reps': 12, 'rest': 60},
    ],
    'Legs': [
        {'exercise': 'Squat', 'sets': 4, 'reps': 6, 'rest': 180},
        {'exercise': 'Romanian Deadlift', 'sets': 3, 'reps': 10, 'rest': 120},
        {'exercise': 'Leg Press', 'sets': 3, 'reps': 12, 'rest': 90},
        {'exercise': 'Leg Curls', 'sets': 3, 'reps': 15, 'rest': 60},
        {'exercise': 'Calf Raises', 'sets': 4, 'reps': 20, 'rest': 60},
    ]
}
</code></code></pre><p><strong>Loading a template:</strong></p><pre><code><code>def load_template(template_name):
    exercises = TEMPLATES[template_name]
    
    print(f"\n{template_name.upper()} DAY:")
    for i, ex in enumerate(exercises, 1):
        print(f"{i}. {ex['exercise']}")
        print(f"   {ex['sets']}&#215;{ex['reps']}")
        print(f"   Rest: {ex['rest']}s")
</code></code></pre><p>We also have other features such as the timer which the user can use to time your workout sessions.</p><h2>What You&#8217;ve Accomplished This Week</h2><p>&#127881; <strong>Congratulations!</strong> You&#8217;ve built a <strong>complete workout tracking system</strong>!</p><ul><li><p><strong>Day 1:</strong> Visual plate calculator</p></li><li><p><strong>Day 2:</strong> Workout logger with charts</p></li><li><p><strong>Day 3:</strong> Complete training dashboard</p></li></ul><p><strong>You now have:</strong></p><p>&#9989; <strong>Plate calculator</strong> - Visual barbell loading<br>&#9989; <strong>Workout logging</strong> - Track all exercises<br>&#9989; <strong>Progress charts</strong> - See gains visualized<br>&#9989; <strong>1RM calculator</strong> - Multiple formulas<br>&#9989; <strong>Strength standards</strong> - Compare your lifts<br>&#9989; <strong>Workout templates</strong> - Pre-built programs<br>&#9989; <strong>Session timers</strong> - Track workout &amp; rest<br>&#9989; <strong>Analytics</strong> - Volume &amp; intensity trends<br>&#9989; <strong>Export reports</strong> - Share your progress<br>&#9989; <strong>Professional GUI</strong> - Tabbed interface</p><p><strong>Next steps:</strong></p><ul><li><p>Add exercise video library</p></li><li><p>Integrate with fitness trackers</p></li><li><p>Build mobile version</p></li><li><p>Add nutrition tracking</p></li><li><p>Social features (share workouts)</p></li><li><p>Online coaching integration</p></li></ul><p>You&#8217;ve built the foundation for a <strong>complete fitness platform</strong>! &#128640;</p><h2>View Code Evolution</h2><p>Compare today&#8217;s complete dashboard with earlier versions and see the full progression from simple calculator &#8594; logger &#8594; complete system!</p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/build-a-workout-tracker-suite-day-c58">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build a Workout Tracker Suite: Day 2- Workout Logger with Live Charts]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/build-a-workout-tracker-suite-day-057</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-workout-tracker-suite-day-057</guid><pubDate>Wed, 20 May 2026 11:51:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/54bbc298-9a93-4227-bce7-d27357716868_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>Workout Tracker Suite</strong> that helps you log lifts, visualize progress, and optimize your training &#8212; perfect for anyone serious about strength training!</p><ul><li><p><strong>Day 1:</strong> Barbell Plate Calculator (Tkinter GUI)</p></li><li><p><strong>Day 2:</strong> Workout Logger with Live Charts <strong>(Today)</strong></p></li><li><p><strong>Day 3:</strong> Complete Workout Dashboard</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-18">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>Yesterday we built a plate calculator. Today we&#8217;re adding <strong>workout logging with live progress charts</strong>! Log your sets, track your PRs, and see your strength gains visualized in real-time!</p><p>You&#8217;ll enhance the app into a complete training tracker with embedded matplotlib charts!</p><h2>Project Task</h2><p>Enhance the plate calculator with workout logging:</p><ul><li><p>Log exercises with sets, reps, weight, and RPE</p></li><li><p>Save workouts to CSV with timestamps</p></li><li><p>Live progress chart showing weight over time</p></li><li><p>Personal records (PR) tracker</p></li><li><p>Exercise history viewer</p></li><li><p>Volume calculations (sets &#215; reps &#215; weight)</p></li><li><p>Filter by exercise and date range</p></li><li><p>Embedded matplotlib charts in Tkinter</p></li><li><p>All Day 1 features still work</p></li></ul><p>This project gives you hands-on practice with CSV data storage, pandas data analysis, matplotlib integration in Tkinter, data visualization, progressive overload tracking, and building data-driven fitness tools &#8212; essential skills for analytics applications!</p><h2>Expected Output</h2><p><strong>Running the enhanced workout logger:</strong></p><pre><code><code>python workout_logger.py
</code></code></pre><p><strong>Log a workout:</strong></p><p>In this version the user can log in a workout such as bench press, squat, etc. These can be chosen from the drop down list:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NYRB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NYRB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png 424w, https://substackcdn.com/image/fetch/$s_!NYRB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png 848w, https://substackcdn.com/image/fetch/$s_!NYRB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png 1272w, https://substackcdn.com/image/fetch/$s_!NYRB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NYRB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:986242,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/198247786?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NYRB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png 424w, https://substackcdn.com/image/fetch/$s_!NYRB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png 848w, https://substackcdn.com/image/fetch/$s_!NYRB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png 1272w, https://substackcdn.com/image/fetch/$s_!NYRB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bc14bea-732b-4222-a34d-e8c229c7aea2_2502x1668.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After some days, you will see a weight progress at the bottom part of the GUI showing how your weights have changed over time:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-YbK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-YbK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png 424w, https://substackcdn.com/image/fetch/$s_!-YbK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png 848w, https://substackcdn.com/image/fetch/$s_!-YbK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png 1272w, https://substackcdn.com/image/fetch/$s_!-YbK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-YbK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/47066338-87db-4984-809d-dbf57089964b_2796x1864.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1226399,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/198247786?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-YbK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png 424w, https://substackcdn.com/image/fetch/$s_!-YbK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png 848w, https://substackcdn.com/image/fetch/$s_!-YbK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png 1272w, https://substackcdn.com/image/fetch/$s_!-YbK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47066338-87db-4984-809d-dbf57089964b_2796x1864.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong>CSV file generated: </strong></p><p>Every time you log in a workout, the data are saved in a local <em>workouts.csv</em> <code>file. </code>For the example demonstrated above, here is how the file looks like:</p><pre><code><code>date,time,exercise,sets,reps,weight,rpe,volume
2026-05-13,14:30:00,Bench Press,3,8,225,8,5400
2026-05-13,14:45:00,Squat,4,6,315,9,7560
2026-05-13,15:00:00,Deadlift,3,5,405,9,6075
2026-05-10,15:15:00,Bench Press,4,6,215,7,5160
2026-05-10,15:30:00,Squat,3,8,295,8,7080

</code></code></pre><h2>Setup Instructions</h2><p><strong>Install Required Package:</strong></p><p></p><pre><code><code>pip install matplotlib pandas</code></code></pre><p><strong>Run the logger:</strong></p><pre><code><code>python workout_logger.py
</code></code></pre><p><strong>Data storage:</strong></p><ul><li><p>Workouts saved to <code>workouts.csv</code> automatically</p></li><li><p>PRs tracked automatically</p></li><li><p>Charts update in real-time</p></li></ul><h2>Understanding CSV Workout Storage</h2><p><strong>Why CSV for workouts?</strong></p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/build-a-workout-tracker-suite-day-057">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build a Workout Tracker Suite: Day 1 - Barbell Plate Calculator ]]></title><description><![CDATA[Today we will build a visual barbell plate calculator with Python that shows you exactly how to load your barbell!]]></description><link>https://dailypythonprojects.substack.com/p/build-a-workout-tracker-suite-day</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-workout-tracker-suite-day</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Tue, 19 May 2026 11:23:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/74fed1f8-f653-40b5-a49a-8210983d9762_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>Workout Tracker Suite</strong> that helps you log lifts, visualize progress, and optimize your training &#8212; perfect for anyone serious about strength training!</p><p><strong>Why build this?</strong> Because tracking workouts manually is tedious, and most apps are overcomplicated. You&#8217;ll create tools that solve real problems: calculating plates, logging progress, and seeing gains visualized!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong>What you&#8217;ll learn:</strong> This series teaches you GUI development, data visualization, mathematical calculations, user interface design, and building practical fitness tools &#8212; essential skills for desktop applications!</p><p><strong>Why users love this:</strong> A plate calculator that shows exactly what your barbell looks like? Instant dopamine! By Day 3, you&#8217;ll have a complete training dashboard that tracks every lift and shows your progress charts in real-time!</p><ul><li><p><strong>Day 1:</strong> Barbell Plate Calculator (Tkinter GUI) <strong>(Today)</strong></p></li><li><p><strong>Day 2:</strong> Workout Logger with Live Charts</p></li><li><p><strong>Day 3:</strong> Complete Workout Dashboard</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-18">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>We&#8217;re starting with something <strong>immediately satisfying</strong>: a visual plate calculator that shows you exactly how to load your barbell! No more mental math at the gym!</p><p>You&#8217;ll build a beautiful GUI that calculates the optimal plate combination and displays your loaded barbell visually!</p><h2>Project Task</h2><p>Create a barbell plate calculator with:</p><ul><li><p>Target weight input (kg or lbs)</p></li><li><p>Visual barbell display showing loaded plates</p></li><li><p>Optimal plate combination calculator</p></li><li><p>Common lift presets (45/135, 60/185, 100/225, etc.)</p></li><li><p>Support for standard plate sets (45, 35, 25, 10, 5, 2.5)</p></li><li><p>Bar weight selection (20kg/45lbs standard, women&#8217;s bar, specialty bars)</p></li><li><p>Save favorite lifts</p></li><li><p>Clean, intuitive Tkinter interface</p></li><li><p>Color-coded plates by weight</p></li><li><p>Real-time updates as you type</p></li></ul><p>This project gives you hands-on practice with Tkinter GUI, canvas drawing, mathematical optimization, event handling, and building visual tools &#8212; essential skills for desktop applications!</p><h2>Expected Output</h2><p>The app runs on the desktop. It lets the user to set the target weight. Based on that, the visual is instantly updated with the correct number of plates and their weights for the target weight:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-jt_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-jt_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png 424w, https://substackcdn.com/image/fetch/$s_!-jt_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png 848w, https://substackcdn.com/image/fetch/$s_!-jt_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png 1272w, https://substackcdn.com/image/fetch/$s_!-jt_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-jt_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:659797,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/198241916?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-jt_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png 424w, https://substackcdn.com/image/fetch/$s_!-jt_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png 848w, https://substackcdn.com/image/fetch/$s_!-jt_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png 1272w, https://substackcdn.com/image/fetch/$s_!-jt_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ed5b9d5-8827-4d79-9918-4378d6111605_2496x1664.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The user can also press the &#8220;Save Current&#8221; button to save a configuration and load it later.<br></p><h2>Setup Instructions</h2><p><strong>No installations needed!</strong></p><p>Uses Python standard library only (Tkinter is built-in):</p><pre><code><code>python plate_calculator.py</code></code></pre><p></p><h2>Understanding Plate Calculation Algorithm</h2><p><strong>The Challenge:</strong></p><p>Given a target weight, find the <strong>minimum number of plates</strong> to load on each side.</p><p><strong>Standard plate sets:</strong></p><p><strong>Pounds (lbs):</strong></p><ul><li><p>45 lbs (red) - biggest</p></li><li><p>35 lbs (yellow)</p></li><li><p>25 lbs (green)</p></li><li><p>10 lbs (white)</p></li><li><p>5 lbs (blue)</p></li><li><p>2.5 lbs (small red)</p></li></ul><p><strong>Kilograms (kg):</strong></p><ul><li><p>25 kg (red)</p></li><li><p>20 kg (blue)</p></li><li><p>15 kg (yellow)</p></li><li><p>10 kg (green)</p></li><li><p>5 kg (white)</p></li><li><p>2.5 kg (small red)</p></li><li><p>1.25 kg (small blue)</p></li></ul><p><strong>Algorithm (Greedy approach):</strong></p><pre><code><code>def calculate_plates(target_weight, bar_weight, plates):
    # Weight needed on plates (both sides)
    plates_weight = target_weight - bar_weight
    
    # Weight per side
    weight_per_side = plates_weight / 2
    
    # Greedy algorithm: largest plates first
    plates_needed = []
    remaining = weight_per_side
    
    for plate in sorted(plates, reverse=True):
        count = int(remaining / plate)
        if count &gt; 0:
            plates_needed.append((plate, count))
            remaining -= plate * count
    
    return plates_needed

# Example
target = 225  # lbs
bar = 45      # lbs
plates = [45, 35, 25, 10, 5, 2.5]

result = calculate_plates(target, bar, plates)
# [(45, 1), (25, 1), (10, 1), (5, 1)]
# = 1&#215;45 + 1&#215;25 + 1&#215;10 + 1&#215;5 = 85 lbs per side
# = 85 &#215; 2 + 45 bar = 215 lbs... wait, that's not 225!
</code></code></pre><p><strong>Problem: Rounding!</strong></p><p>225 - 45 = 180 lbs plates / 2 = 90 lbs per side</p><p>But 1&#215;45 + 1&#215;25 + 1&#215;10 + 1&#215;5 = 85 lbs per side (10 lbs short!)</p><p><strong>Solution: Round to nearest 5 lbs (smallest plate &#215; 2)</strong></p><pre><code><code>def calculate_plates(target, bar, plates):
    # Round to nearest achievable weight
    smallest_increment = min(plates) * 2  # 2.5 &#215; 2 = 5 lbs
    target = round(target / smallest_increment) * smallest_increment
    
    weight_per_side = (target - bar) / 2
    
    # ... rest of algorithm
</code></code></pre><h2>Understanding Tkinter Canvas Drawing</h2><p><strong>Why Canvas?</strong></p><p>Tkinter Canvas lets us draw custom graphics:</p><pre><code><code>import tkinter as tk

root = tk.Tk()
canvas = tk.Canvas(root, width=800, height=200, bg='white')
canvas.pack()

# Draw rectangle (plate)
canvas.create_rectangle(100, 50, 150, 150, fill='red', outline='black')

# Draw circle
canvas.create_oval(300, 75, 350, 125, fill='blue')

# Draw line (barbell)
canvas.create_line(50, 100, 750, 100, fill='gray', width=10)

# Draw text
canvas.create_text(400, 50, text="225 lbs", font=('Arial', 16, 'bold'))

root.mainloop()
</code></code></pre><p><strong>Our barbell drawing:</strong></p><pre><code><code>def draw_barbell(canvas, plates_per_side):
    canvas.delete('all')  # Clear previous
    
    # Bar dimensions
    bar_length = 600
    bar_x_start = 100
    bar_y = 100
    
    # Draw bar
    canvas.create_line(
        bar_x_start, bar_y,
        bar_x_start + bar_length, bar_y,
        fill='gray', width=15
    )
    
    # Draw plates on left side
    x_offset = bar_x_start - 10
    for plate_weight, count in plates_per_side:
        for _ in range(count):
            color = get_plate_color(plate_weight)
            width = get_plate_width(plate_weight)
            height = get_plate_height(plate_weight)
            
            # Draw plate
            canvas.create_rectangle(
                x_offset - width, bar_y - height/2,
                x_offset, bar_y + height/2,
                fill=color, outline='black', width=2
            )
            
            # Label
            canvas.create_text(
                x_offset - width/2, bar_y - height/2 - 15,
                text=str(plate_weight), font=('Arial', 10, 'bold')
            )
            
            x_offset -= width + 5
    
    # Mirror for right side
    # ...
</code></code></pre><h2>Understanding Saved Lifts</h2><p><strong>Persistence with JSON:</strong></p><pre><code><code>import json
from pathlib import Path

SAVED_LIFTS_FILE = 'saved_lifts.json'

def save_lift(name, weight, unit):
    # Load existing
    if Path(SAVED_LIFTS_FILE).exists():
        with open(SAVED_LIFTS_FILE, 'r') as f:
            lifts = json.load(f)
    else:
        lifts = {}
    
    # Add new
    lifts[name] = {'weight': weight, 'unit': unit}
    
    # Save
    with open(SAVED_LIFTS_FILE, 'w') as f:
        json.dump(lifts, f, indent=2)

def load_lifts():
    if Path(SAVED_LIFTS_FILE).exists():
        with open(SAVED_LIFTS_FILE, 'r') as f:
            return json.load(f)
    return {}

# Usage
save_lift('Squat', 315, 'lbs')
save_lift('Bench', 225, 'lbs')

lifts = load_lifts()
# {'Squat': {'weight': 315, 'unit': 'lbs'}, ...}
</code></code></pre><h2>Practical Use Cases</h2><p><strong>1. Pre-planning workouts:</strong></p><pre><code><code>Before gym: Check what plates you need
At gym: Load bar exactly right, no confusion
</code></code></pre><p><strong>2. Progressive overload:</strong></p><pre><code><code>Current: 225 lbs
Next week: 230 lbs
Calculator: Shows you need 2.5 lb plates!
</code></code></pre><p><strong>3. Home gym setup:</strong></p><pre><code><code>See which plates you use most
Buy optimal plate set
Avoid buying unnecessary weights
</code></code></pre><p><strong>4. Teaching form:</strong></p><pre><code><code>Show beginners what "135" looks like
Visual reference for proper loading
Safety check before lifting
</code></code></pre><p><strong>5. Multiple lifters:</strong></p><pre><code><code>Save each person's working weights
Quick load for different people
No mental math during workout
</code></code></pre><h2>Coming Tomorrow</h2><p>Tomorrow we&#8217;re adding <strong>workout logging</strong> with live progress charts! Log your sets/reps/weight and see your strength gains visualized in real-time. The plate calculator becomes part of a complete training tool!</p><h2>Skeleton and Solution</h2><p>Below you will find both a downloadable skeleton.py file to help you code the project with comment guides and the downloadable solution.py file containing the correct solution.</p><p>Get the code skeleton here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/view/j9o5i5jp-IWRwzewAnRJlQ&quot;,&quot;text&quot;:&quot;View Code Skeleton&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/view/j9o5i5jp-IWRwzewAnRJlQ"><span>View Code Skeleton</span></a></p><p></p><p>Get the code solution here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/evolution/cybvuP1g3cKY8t7vki6L5A&quot;,&quot;text&quot;:&quot;View Code Solution&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/evolution/cybvuP1g3cKY8t7vki6L5A"><span>View Code Solution</span></a></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[AI-Powered Daily Journal: Day 3 - Insights Dashboard]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/ai-powered-daily-journal-day-3-insights</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/ai-powered-daily-journal-day-3-insights</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Thu, 14 May 2026 18:33:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/959c0a0f-9a48-4dd7-aa20-f83c6ab5d0d3_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build an <strong>AI-Powered Daily Journal</strong> that writes markdown entries, analyzes your mood with AI, and visualizes your emotional patterns over time!</p><ul><li><p><strong>Day 1:</strong> Markdown Journal Writer</p></li><li><p><strong>Day 2:</strong> AI Mood &amp; Theme Analyzer</p></li><li><p><strong>Day 3:</strong> Insights Dashboard <strong>(Today)</strong></p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-17">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p><strong>Welcome to the finale!</strong> We&#8217;ve built a journal with AI analysis. Today we&#8217;re creating a <strong>beautiful Streamlit dashboard</strong> with mood charts, word clouds, and AI-generated monthly summaries!</p><p>You&#8217;ll visualize your emotional journey and discover patterns you never noticed!</p><h2>Project Task</h2><p>Create a Streamlit insights dashboard with:</p><ul><li><p>Mood-over-time line chart (interactive with Plotly)</p></li><li><p>Word cloud from all journal entries</p></li><li><p>Monthly summary cards (AI-generated)</p></li><li><p>Theme frequency chart</p></li><li><p>Entry statistics and streaks</p></li><li><p>Filter by date range</p></li><li><p>Export insights as PDF</p></li><li><p>Beautiful, intuitive interface</p></li></ul><p>This project gives you hands-on practice with Streamlit, data visualization, Plotly charts, word clouds, dashboard design, and building data-driven applications &#8212; essential skills for analytics and insight tools!</p><h2>Expected Output</h2><p><strong>Running the dashboard:</strong></p><pre><code><code>streamlit run dashboard.py</code></code></pre><p><strong>Browser opens with dashboard showing your mood in a graph:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sQdM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sQdM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png 424w, https://substackcdn.com/image/fetch/$s_!sQdM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png 848w, https://substackcdn.com/image/fetch/$s_!sQdM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png 1272w, https://substackcdn.com/image/fetch/$s_!sQdM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sQdM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:519663,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/197734871?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sQdM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png 424w, https://substackcdn.com/image/fetch/$s_!sQdM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png 848w, https://substackcdn.com/image/fetch/$s_!sQdM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png 1272w, https://substackcdn.com/image/fetch/$s_!sQdM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73ecf67-c077-4297-967e-b5891d5d23aa_2694x1796.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Scrolling down the webpage you will find the score for the average mood, a word cloud showing the most used words in your journal, and the top themes you have written about:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OfrT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OfrT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png 424w, https://substackcdn.com/image/fetch/$s_!OfrT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png 848w, https://substackcdn.com/image/fetch/$s_!OfrT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png 1272w, https://substackcdn.com/image/fetch/$s_!OfrT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OfrT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:940675,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/197734871?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OfrT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png 424w, https://substackcdn.com/image/fetch/$s_!OfrT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png 848w, https://substackcdn.com/image/fetch/$s_!OfrT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png 1272w, https://substackcdn.com/image/fetch/$s_!OfrT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7c7e6c9-8ce8-43e6-b2f7-d9c62ddfdb98_2742x1828.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p></p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install streamlit plotly wordcloud langchain-google-genai</code></code></pre><p><strong>Set Google API Key:</strong></p><pre><code><code>export GOOGLE_API_KEY="your-key-here"</code></code></pre><p><strong>Run the dashboard:</strong></p><pre><code><code>streamlit run dashboard.py</code></code></pre><p>Browser automatically opens to </p><p>http://localhost:8501</p><p>Make sure you have some jornal entries in the same directory with the dashboard.py file. Here are some sample files you can use:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://drive.google.com/file/d/18oULtiYyBSIZaxZ6fGiiN1Xdm08iqALv/view?usp=sharing&quot;,&quot;text&quot;:&quot;Download Data&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://drive.google.com/file/d/18oULtiYyBSIZaxZ6fGiiN1Xdm08iqALv/view?usp=sharing"><span>Download Data</span></a></p><p></p><h2>Understanding Streamlit Layout</h2><p><strong>Dashboard structure:</strong></p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/ai-powered-daily-journal-day-3-insights">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[AI-Powered Daily Journal: Day 2 - AI Mood & Theme Analyzer]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/ai-powered-daily-journal-day-2-ai</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/ai-powered-daily-journal-day-2-ai</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Wed, 13 May 2026 17:15:48 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/fa54f694-4fc1-4b4a-891b-d0c86120397d_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build an <strong>AI-Powered Daily Journal</strong> that writes markdown entries, analyzes your mood with AI, and visualizes your emotional patterns over time!</p><ul><li><p><strong>Day 1:</strong> Markdown Journal Writer</p></li><li><p><strong>Day 2:</strong> AI Mood &amp; Theme Analyzer <strong>(Today)</strong></p></li><li><p><strong>Day 3:</strong> Insights Dashboard</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-17">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>Yesterday we built a markdown journal. Today we&#8217;re adding <strong>AI-powered analysis</strong> &#8212; Gemini reads each entry, scores your mood from -1 (negative) to +1 (positive), and extracts 3-5 recurring themes!</p><p>All analysis is cached as JSON metadata, so re-running is instant and free!</p><h2>Project Task</h2><p>Enhance the journal with AI analysis that:</p><ul><li><p>New <code>review</code> command analyzes entries</p></li><li><p>Uses Gemini to score mood (-1 to 1 scale)</p></li><li><p>Extracts 3-5 recurring themes per entry</p></li><li><p>Caches analysis as JSON metadata files</p></li><li><p>Re-runs are instant (reads from cache)</p></li><li><p>Shows mood trends over time</p></li><li><p>Lists most common themes across all entries</p></li><li><p>Clean output with mood emoji indicators</p></li><li><p>All Day 1 features still work</p></li></ul><p>This project gives you hands-on practice with AI analysis, sentiment analysis, theme extraction, JSON caching, metadata management, and building intelligent tools &#8212; essential skills for AI-powered applications!</p><h2>Expected Output</h2><p><strong>Analyzing entries for the first time:</strong></p><pre><code><code>python journal.py review</code></code></pre><p><strong>Console Output:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_yca!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_yca!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png 424w, https://substackcdn.com/image/fetch/$s_!_yca!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png 848w, https://substackcdn.com/image/fetch/$s_!_yca!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png 1272w, https://substackcdn.com/image/fetch/$s_!_yca!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_yca!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png" width="1372" height="2048" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2048,&quot;width&quot;:1372,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:459571,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/197544274?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_yca!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png 424w, https://substackcdn.com/image/fetch/$s_!_yca!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png 848w, https://substackcdn.com/image/fetch/$s_!_yca!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png 1272w, https://substackcdn.com/image/fetch/$s_!_yca!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6930018e-badd-4e48-a9f9-b18506280c42_1372x2048.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZNmL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZNmL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png 424w, https://substackcdn.com/image/fetch/$s_!ZNmL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png 848w, https://substackcdn.com/image/fetch/$s_!ZNmL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png 1272w, https://substackcdn.com/image/fetch/$s_!ZNmL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZNmL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png" width="1372" height="1714" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1714,&quot;width&quot;:1372,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:366598,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/197544274?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ZNmL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png 424w, https://substackcdn.com/image/fetch/$s_!ZNmL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png 848w, https://substackcdn.com/image/fetch/$s_!ZNmL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png 1272w, https://substackcdn.com/image/fetch/$s_!ZNmL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2980761-725f-419b-a7d7-e1169836fa70_1372x1714.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Setup Instructions</h2><p><strong>Install Required Package:</strong></p><pre><code><code>pip install langchain-google-genai</code></code></pre><p><strong>Get Your Google API Key:</strong></p><ol><li><p>Go to <a href="https://aistudio.google.com/app/apikey">Google AI Studio</a></p></li><li><p>Click &#8220;Create API Key&#8221;</p></li><li><p>Copy your key</p></li><li><p>Set environment variable:</p></li></ol><pre><code><code># Mac/Linux
export GOOGLE_API_KEY="your-key-here"

# Windows (PowerShell)
$env:GOOGLE_API_KEY="your-key-here"
</code></code></pre><p><strong>Or edit the script:</strong></p><p>This is a simpler method to provide the API key, but less secure.</p><pre><code><code>GOOGLE_API_KEY = "your-key-here"  # Paste your key</code></code></pre><p><strong>Run analysis:</strong></p><pre><code><code>python journal.py review
</code></code></pre><h2>Understanding AI Mood Analysis</h2><p><strong>What is mood scoring?</strong></p><p>The AI reads your entry and assigns a score from -1 to 1:</p><p></p><pre><code><code> 1.0  Very positive   &#128516;  Joyful, excited, grateful, accomplished
 0.5  Positive        &#128578;  Happy, content, optimistic
 0.0  Neutral         &#128528;  Balanced, matter-of-fact
-0.5  Negative        &#128532;  Sad, frustrated, disappointed
-1.0  Very negative   &#128546;  Devastated, hopeless, extremely upset
</code></code></pre><p><strong>How it works:</strong></p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/ai-powered-daily-journal-day-2-ai">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[AI-Powered Daily Journal: Day 1 - Markdown Journal Writer]]></title><description><![CDATA[Date-based journal that greets you with a randomized reflection prompt and saves each entry to a YYYY-MM-DD.md file under a journal/ folder. Includes commands to list, search, and open past entries.]]></description><link>https://dailypythonprojects.substack.com/p/ai-powered-daily-journal-day-1-markdown</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/ai-powered-daily-journal-day-1-markdown</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Tue, 12 May 2026 16:14:04 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/62664104-68d9-43f7-a15a-5055774b1d78_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build an <strong>AI-Powered Daily Journal</strong> that writes markdown entries, analyzes your mood with AI, and visualizes your emotional patterns over time!</p><p><strong>Why build this?</strong> Because journaling is powerful for mental clarity and self-reflection. You&#8217;ll create a tool that not only stores your thoughts but also uses AI to understand them and show you patterns you might miss!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong>What you&#8217;ll learn:</strong> This series teaches you file-based data storage, markdown formatting, CLI tool development, AI sentiment analysis, data visualization, and building applications that provide real insights &#8212; essential skills for personal productivity tools!</p><p><strong>Why users love this:</strong> A journal that understands you! By Day 3, you&#8217;ll have mood charts, word clouds, and AI-generated summaries showing your emotional journey and recurring themes. It&#8217;s like having a therapist and data analyst in one!</p><ul><li><p><strong>Day 1:</strong> Markdown Journal Writer <strong>(Today)</strong></p></li><li><p><strong>Day 2:</strong> AI Mood &amp; Theme Analyzer</p></li><li><p><strong>Day 3:</strong> Insights Dashboard</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-17">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>We&#8217;re starting with the foundation: a command-line journal that greets you with randomized reflection prompts, saves entries as dated markdown files, and lets you search through your past thoughts! <strong>We will not use AI today yet.</strong></p><p>You&#8217;ll learn file-based storage, markdown formatting, and building a clean CLI for daily use!</p><h2>Project Task</h2><p>Create a markdown journal system that:</p><ul><li><p>Date-based entries (one file per day: <code>YYYY-MM-DD.md</code>)</p></li><li><p>Stores entries in <code>journal/</code> folder</p></li><li><p>Randomized reflection prompts</p></li><li><p>Commands: <code>new</code>, <code>list</code>, <code>search</code>, <code>open</code></p></li><li><p>Markdown formatting for entries</p></li><li><p>Timestamp each entry</p></li><li><p>Clean CLI interface with argparse</p></li><li><p>Search through past entries</p></li><li><p>Open entries in your editor</p></li></ul><p>This project gives you hands-on practice with file organization, markdown formatting, command-line tools, argparse, pathlib, datetime handling, and building daily-use tools &#8212; essential skills for productivity applications!</p><h2>Expected Output</h2><p><strong>Creating a new journal entry:</strong></p><pre><code><code>python journal.py new</code></code></pre><p><strong>Console Output:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!x10D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!x10D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png 424w, https://substackcdn.com/image/fetch/$s_!x10D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png 848w, https://substackcdn.com/image/fetch/$s_!x10D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png 1272w, https://substackcdn.com/image/fetch/$s_!x10D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!x10D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png" width="1372" height="1228" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1228,&quot;width&quot;:1372,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:267231,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/197370213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!x10D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png 424w, https://substackcdn.com/image/fetch/$s_!x10D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png 848w, https://substackcdn.com/image/fetch/$s_!x10D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png 1272w, https://substackcdn.com/image/fetch/$s_!x10D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3ac634f-8d68-48dc-ba13-c5a6d47355b6_1372x1228.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><br></p><p><strong>Generated file: </strong><code>journal/2026-05-12.md</code></p><pre><code><code># Friday, April 26, 2026

## &#128221; Entry 1 - 10:30 AM

**Prompt:** What's one small thing that brought you joy today?

Had a great coffee this morning at the new cafe downtown.
The barista remembered my order - made me feel like a regular.
Spent the afternoon coding a new project. Made real progress!
Feeling accomplished and energized.

---
</code></code></pre><p><strong>Listing all entries:</strong></p><pre><code><code>python journal.py list</code></code></pre><p><strong>Output:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Mcni!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Mcni!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png 424w, https://substackcdn.com/image/fetch/$s_!Mcni!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png 848w, https://substackcdn.com/image/fetch/$s_!Mcni!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png 1272w, https://substackcdn.com/image/fetch/$s_!Mcni!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Mcni!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png" width="1372" height="1116" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1116,&quot;width&quot;:1372,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:256462,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/197370213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Mcni!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png 424w, https://substackcdn.com/image/fetch/$s_!Mcni!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png 848w, https://substackcdn.com/image/fetch/$s_!Mcni!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png 1272w, https://substackcdn.com/image/fetch/$s_!Mcni!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f0c4d2d-b1cc-406f-a8e7-fdde7ed014a3_1372x1116.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong>Searching entries:</strong></p><pre><code><code>python journal.py search "coffee"
</code></code></pre><p><strong>Output:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IDK_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IDK_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png 424w, https://substackcdn.com/image/fetch/$s_!IDK_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png 848w, https://substackcdn.com/image/fetch/$s_!IDK_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png 1272w, https://substackcdn.com/image/fetch/$s_!IDK_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IDK_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png" width="1372" height="1378" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1378,&quot;width&quot;:1372,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:253601,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/197370213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IDK_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png 424w, https://substackcdn.com/image/fetch/$s_!IDK_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png 848w, https://substackcdn.com/image/fetch/$s_!IDK_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png 1272w, https://substackcdn.com/image/fetch/$s_!IDK_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd465b507-dbb0-45be-a40e-21c9f67915a7_1372x1378.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><br></p><p><strong>Opening a specific entry:</strong></p><pre><code><code>python journal.py open 2026-04-26</code></code></pre><p><strong>Output:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9uil!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9uil!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png 424w, https://substackcdn.com/image/fetch/$s_!9uil!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png 848w, https://substackcdn.com/image/fetch/$s_!9uil!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png 1272w, https://substackcdn.com/image/fetch/$s_!9uil!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9uil!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png" width="1372" height="1116" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1116,&quot;width&quot;:1372,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:232723,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/197370213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9uil!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png 424w, https://substackcdn.com/image/fetch/$s_!9uil!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png 848w, https://substackcdn.com/image/fetch/$s_!9uil!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png 1272w, https://substackcdn.com/image/fetch/$s_!9uil!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F381ec866-597b-4ab7-a053-a8cae944b73e_1372x1116.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Setup Instructions</h2><p><strong>No installations needed!</strong></p><p>Uses Python standard library only:</p><pre><code><code>python journal.py new</code></code></pre><p><strong>The journal creates:</strong></p><ul><li><p><code>journal/</code> folder (auto-created on first use)</p></li><li><p>One <code>.md</code> file per day (e.g., <code>2026-05-12.md</code>)</p></li><li><p>Each file contains all entries for that day</p></li></ul><h2>Understanding File-Based Journaling</h2><p><strong>Why file-based?</strong></p><p>&#9989; <strong>Simple</strong> - Just markdown files, no database<br>&#9989; <strong>Portable</strong> - Copy your <code>journal/</code> folder anywhere<br>&#9989; <strong>Human-readable</strong> - Open files in any text editor<br>&#9989; <strong>Git-friendly</strong> - Version control your thoughts<br>&#9989; <strong>Privacy</strong> - Files stay on your computer<br>&#9989; <strong>Future-proof</strong> - Markdown will always be readable</p><p><strong>File structure:</strong></p><pre><code><code>your-project/
&#9500;&#9472;&#9472; journal.py
&#9492;&#9472;&#9472; journal/
    &#9500;&#9472;&#9472; 2026-04-26.md
    &#9500;&#9472;&#9472; 2026-04-25.md
    &#9500;&#9472;&#9472; 2026-04-24.md
    &#9492;&#9472;&#9472; ...
</code></code></pre><p><strong>Why YYYY-MM-DD format?</strong></p><p>Putting the year before is usually better in programming and working with files:</p><pre><code><code>2026-04-26.md  &#9989; Sorts chronologically
26-04-2026.md  &#10060; Sorts incorrectly
april-26.md    &#10060; No year, doesn't sort
</code></code></pre><h2>Understanding pathlib</h2><p><strong>Modern file handling with pathlib:</strong></p><pre><code><code>from pathlib import Path

# Create journal directory
journal_dir = Path("journal")
journal_dir.mkdir(exist_ok=True)  # Create if doesn't exist

# Build file path
date_str = "2026-04-26"
file_path = journal_dir / f"{date_str}.md"
# Result: journal/2026-04-26.md

# Check if file exists
if file_path.exists():
    print("Entry already exists!")

# Read file
content = file_path.read_text()

# Write file
file_path.write_text(content)

# List all journal files
entries = sorted(journal_dir.glob("*.md"))
</code></code></pre><p><strong>Why pathlib over os.path?</strong></p><pre><code><code># OLD way (os.path)
import os
path = os.path.join("journal", date_str + ".md")

# NEW way (pathlib)
from pathlib import Path
path = Path("journal") / f"{date_str}.md"
</code></code></pre><p>&#9989; <strong>Cleaner syntax</strong> - <code>/</code> operator joins paths<br>&#9989; <strong>Object-oriented</strong> - Methods like <code>.exists()</code>, <code>.read_text()</code><br>&#9989; <strong>Cross-platform</strong> - Handles Windows/Mac/Linux differences</p><h2>Understanding argparse for CLI</h2><p><strong>Building a professional CLI:</strong></p><pre><code><code>import argparse

parser = argparse.ArgumentParser(description="AI Daily Journal")

# Add subcommands
subparsers = parser.add_subparsers(dest='command')

# 'new' command
new_parser = subparsers.add_parser('new', help='Create new entry')

# 'list' command
list_parser = subparsers.add_parser('list', help='List all entries')
list_parser.add_argument('--all', action='store_true', help='Show all entries')

# 'search' command
search_parser = subparsers.add_parser('search', help='Search entries')
search_parser.add_argument('query', help='Search term')

# 'open' command
open_parser = subparsers.add_parser('open', help='Open entry')
open_parser.add_argument('date', help='Date (YYYY-MM-DD)')

# Parse arguments
args = parser.parse_args()

if args.command == 'new':
    create_entry()
elif args.command == 'list':
    list_entries(show_all=args.all)
elif args.command == 'search':
    search_entries(args.query)
elif args.command == 'open':
    open_entry(args.date)
</code></code></pre><p><strong>Result:</strong></p><pre><code><code>python journal.py new              # Create entry
python journal.py list             # List recent
python journal.py list --all       # List all
python journal.py search "coffee"  # Search
python journal.py open 2026-04-26  # Open
</code></code></pre><h2>Understanding Reflection Prompts</h2><p><strong>Why prompts?</strong></p><p>Random prompts help you:</p><ul><li><p>Break through writer&#8217;s block</p></li><li><p>Explore different aspects of your day</p></li><li><p>Maintain variety in entries</p></li><li><p>Discover new insights</p></li></ul><p><strong>Our prompt bank:</strong></p><pre><code><code>REFLECTION_PROMPTS = [
    "What's one small thing that brought you joy today?",
    "What challenged you today, and what did you learn from it?",
    "What are you grateful for right now?",
    "What's on your mind that you need to get out?",
    "What progress did you make today, no matter how small?",
    "What would you do differently if you could redo today?",
    "Who or what inspired you today?",
    "What's something you're looking forward to?",
    "What did you do today that aligned with your values?",
    "What's one thing you want to remember about today?"
]
</code></code></pre><p><strong>Selecting a random prompt:</strong></p><pre><code><code>import random

prompt = random.choice(REFLECTION_PROMPTS)
print(f"&#128173; Reflection Prompt:\n\"{prompt}\"")
</code></code></pre><h2>Understanding Markdown Formatting</h2><p><strong>Why markdown for journals?</strong></p><p>&#9989; <strong>Readable</strong> - Plain text with simple formatting<br>&#9989; <strong>Portable</strong> - Works everywhere<br>&#9989; <strong>Structured</strong> - Headers, lists, emphasis<br>&#9989; <strong>Future-proof</strong> - Never becomes obsolete</p><p><strong>Our entry structure:</strong></p><pre><code><code># Friday, April 26, 2026          &#8592; Day header

## &#128221; Entry 1 - 10:30 AM          &#8592; Entry header

**Prompt:** What brought you joy? &#8592; Prompt (bold)

Entry text goes here...           &#8592; User's writing
Multiple paragraphs supported.

---                               &#8592; Separator
</code></code></pre><p><strong>Markdown elements we use:</strong></p><pre><code><code># H1 Header          &#8594; Day title
## H2 Header         &#8594; Entry number
**bold**            &#8594; Prompt label
---                 &#8594; Horizontal rule (separator)
</code></code></pre><h2>Understanding Entry Metadata</h2><p><strong>What we track:</strong></p><pre><code><code>entry_data = {
    'date': '2026-04-26',
    'day_name': 'Friday',
    'entry_number': 1,
    'timestamp': '10:30 AM',
    'prompt': 'What brought you joy today?',
    'content': '...',
    'word_count': 32,
    'char_count': 189
}
</code></code></pre><p><strong>Why track this:</strong></p><ul><li><p>Know how many entries per day</p></li><li><p>See when you journal (morning/evening patterns)</p></li><li><p>Track writing volume over time</p></li><li><p>Tomorrow: AI will analyze this data!</p></li></ul><h2>Practical Use Cases</h2><p><strong>1. Daily reflection:</strong></p><pre><code><code># Every morning
python journal.py new
# Write about yesterday, plan for today
</code></code></pre><p><strong>2. Gratitude practice:</strong></p><pre><code><code># Focus on positive moments
python journal.py new
# Prompt guides you to find joy
</code></code></pre><p><strong>3. Problem processing:</strong></p><pre><code><code># When stressed or confused
python journal.py new
# Writing clarifies thoughts
</code></code></pre><p><strong>4. Review patterns:</strong></p><pre><code><code># Look back at your month
python journal.py list
# See frequency and themes
</code></code></pre><p><strong>5. Find specific memories:</strong></p><pre><code><code># Remember that conversation?
python journal.py search "conversation"
# Find when you wrote about it
</code></code></pre><h2>Coming Tomorrow</h2><p>Tomorrow we&#8217;re adding <strong>AI mood and theme analysis</strong> with Gemini! The AI will read each entry, score your mood from -1 (negative) to +1 (positive), and extract 3-5 recurring themes. All cached as JSON so re-runs are free!</p><h2>Skeleton and Solution</h2><p>Below you will find both a downloadable skeleton.py file to help you code the project with comment guides and the downloadable solution.py file containing the correct solution.</p><p>Get the code skeleton here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/view/SwLEaQ2yRXbCUfJ3v_N7sQ&quot;,&quot;text&quot;:&quot;View Skeleton&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/view/SwLEaQ2yRXbCUfJ3v_N7sQ"><span>View Skeleton</span></a></p><p></p><p>Get the code solution here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/evolution/YlEP5nvpeInO18ujOP1uCQ&quot;,&quot;text&quot;:&quot;View Code Solution&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/evolution/YlEP5nvpeInO18ujOP1uCQ"><span>View Code Solution</span></a></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Desktop Notification System: Day 3 - Smart Notification Manager with Persistence]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/desktop-notification-system-day-3</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/desktop-notification-system-day-3</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Thu, 07 May 2026 14:52:55 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/22ec9163-4fb1-42fb-96c7-7f845a0ab0b9_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>Desktop Notification System</strong> that displays toast notifications, schedules reminders, and manages alerts &#8212; perfect for building productivity tools!</p><ul><li><p><strong>Day 1:</strong> Simple Desktop Notifications</p></li><li><p><strong>Day 2:</strong> Scheduled Notifications &amp; Timers</p></li><li><p><strong>Day 3:</strong> Smart Notification Manager with Persistence <strong>(Today)</strong></p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-16">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p><strong>Welcome to the finale!</strong> We&#8217;ve built notifications and timers. Today we&#8217;re creating a <strong>complete notification management system</strong> with saved reminders, templates, and a persistent scheduler that remembers everything!</p><p>You&#8217;ll build a manager that saves your notifications, loads them on startup, and runs multiple scheduled reminders simultaneously!</p><h2>Project Task</h2><p>Create a complete notification management system with:</p><ul><li><p>Save notifications to JSON file</p></li><li><p>Load saved notifications on startup</p></li><li><p>Manage multiple scheduled notifications</p></li><li><p>Create reusable notification templates</p></li><li><p>List all active/scheduled notifications</p></li><li><p>Cancel specific notifications</p></li><li><p>Edit and update notifications</p></li><li><p>Persistent storage between sessions</p></li><li><p>Clean command-line interface</p></li></ul><p>This project gives you hands-on practice with JSON persistence, data management, state tracking, CRUD operations, and building production-ready tools &#8212; essential skills for real-world applications!</p><h2>Expected Output</h2><p><strong>Running the notification manager:</strong></p><pre><code><code>python solution.py</code></code></pre><p><strong>Console Output:</strong></p><p>First you need to set up a scheduled notification in the terminal. Notice that we are also creating a template out of it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GjSG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GjSG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png 424w, https://substackcdn.com/image/fetch/$s_!GjSG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png 848w, https://substackcdn.com/image/fetch/$s_!GjSG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png 1272w, https://substackcdn.com/image/fetch/$s_!GjSG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GjSG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1354771,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/196787062?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GjSG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png 424w, https://substackcdn.com/image/fetch/$s_!GjSG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png 848w, https://substackcdn.com/image/fetch/$s_!GjSG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png 1272w, https://substackcdn.com/image/fetch/$s_!GjSG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1715bb4f-ac76-4468-a8fc-437b542e714a_2508x1672.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Then, the notification will trigger as a native desktop notification:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bfIo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bfIo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png 424w, https://substackcdn.com/image/fetch/$s_!bfIo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png 848w, https://substackcdn.com/image/fetch/$s_!bfIo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png 1272w, https://substackcdn.com/image/fetch/$s_!bfIo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bfIo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:902618,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/196787062?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!bfIo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png 424w, https://substackcdn.com/image/fetch/$s_!bfIo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png 848w, https://substackcdn.com/image/fetch/$s_!bfIo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png 1272w, https://substackcdn.com/image/fetch/$s_!bfIo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8aeec7-1652-458e-93b6-0870d0c4eafe_1854x1236.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p></p><p><strong>Saved JSON file: </strong><code>notifications_data.json</code></p><pre><code><code>{
  "notifications": [
    {
      "id": "notif_001",
      "title": "Team Meeting",
      "message": "Weekly sync starts now",
      "type": "scheduled",
      "time": "14:00",
      "status": "active",
      "created_at": "2026-04-26T10:15:00",
      "template_name": "Weekly Team Meeting"
    },
    {
      "id": "notif_002",
      "title": "Hydration Reminder",
      "message": "Time to drink water!",
      "type": "recurring",
      "interval_minutes": 60,
      "status": "active",
      "created_at": "2026-04-26T10:20:00",
      "template_name": "Hydration Reminder"
    }
  ],
  "templates": [
    {
      "name": "Daily Standup",
      "type": "scheduled",
      "time": "09:30",
      "title": "Daily Standup",
      "message": "Morning team sync",
      "use_count": 15
    },
    {
      "name": "Hydration Reminder",
      "type": "recurring",
      "interval_minutes": 60,
      "title": "Hydration Reminder",
      "message": "Time to drink water!",
      "use_count": 5
    }
  ]
}
</code></code></pre><p></p><h2>Setup Instructions</h2><p><strong>No new installations needed!</strong></p><p>Same setup as Days 1 &amp; 2:</p><p><strong>macOS:</strong></p><pre><code><code>python solution.py
</code></code></pre><p><strong>Windows:</strong></p><pre><code><code>pip install win10toast  # If not already installed
python solution.py
</code></code></pre><p><strong>Linux:</strong></p><pre><code><code>python solution.py
</code></code></pre><p><strong>Data file:</strong></p><ul><li><p>Creates <code>notifications_data.json</code> automatically</p></li><li><p>Loads on startup</p></li><li><p>Saves on exit or after changes</p></li></ul><h2>Understanding Data Persistence</h2><p><strong>Why persist data?</strong></p><p>Without persistence:</p><ul><li><p>&#10060; All notifications lost when program closes</p></li><li><p>&#10060; Must recreate reminders every time</p></li><li><p>&#10060; Can&#8217;t track notification history</p></li></ul><p>With persistence:</p><ul><li><p>&#9989; Notifications survive program restarts</p></li><li><p>&#9989; Templates saved for reuse</p></li><li><p>&#9989; History of completed notifications</p></li><li><p>&#9989; Resume where you left off</p></li></ul><p><strong>Our approach:</strong></p><pre><code><code># On startup
def load():
    with open('notifications_data.json', 'r') as f:
        data = json.load(f)
    return data

# On exit or after changes
def save():
    with open('notifications_data.json', 'w') as f:
        json.dump(data, f, indent=2)
</code></code></pre><h2>Understanding the Data Structure</h2><p><strong>notifications_data.json structure:</strong></p><pre><code><code>{
  "notifications": [
    {
      "id": "notif_001",           // Unique identifier
      "title": "Meeting",          // Notification title
      "message": "Team sync",      // Notification message
      "type": "scheduled",         // Type: scheduled/recurring/instant
      "time": "14:00",            // For scheduled notifications
      "interval_minutes": 60,      // For recurring notifications
      "status": "active",          // Status: active/completed/cancelled
      "created_at": "2026-04-26T10:15:00",
      "template_name": "Weekly Meeting"  // Optional
    }
  ],
  "templates": [
    {
      "name": "Daily Standup",     // Template name
      "type": "scheduled",         // Template type
      "time": "09:30",            // Template time
      "title": "Daily Standup",    // Default title
      "message": "Morning sync",   // Default message
      "use_count": 15             // Usage tracking
    }
  ]
}
</code></code></pre><p><strong>Key fields:</strong></p><ul><li><p><strong>id</strong> - Unique identifier (e.g., <code>notif_001</code>)</p></li><li><p><strong>type</strong> - <code>scheduled</code>, <code>recurring</code>, <code>instant</code>, <code>countdown</code></p></li><li><p><strong>status</strong> - <code>active</code>, <code>completed</code>, <code>cancelled</code></p></li><li><p><strong>created_at</strong> - When notification was created</p></li><li><p><strong>template_name</strong> - Links to template (optional)</p></li></ul><h2>Understanding Notification States</h2><p><strong>Notification lifecycle:</strong></p><pre><code><code>Created &#8594; Active &#8594; Completed
           &#8595;
       Cancelled
</code></code></pre><p><strong>State definitions:</strong></p><p><strong>Active:</strong></p><ul><li><p>Scheduled in the future</p></li><li><p>Currently recurring</p></li><li><p>Waiting to be sent</p></li></ul><p><strong>Completed:</strong></p><ul><li><p>Already sent</p></li><li><p>Countdown finished</p></li><li><p>One-time notification done</p></li></ul><p><strong>Cancelled:</strong></p><ul><li><p>User manually cancelled</p></li><li><p>Removed before sending</p></li></ul><p><strong>State transitions:</strong></p><pre><code><code># Create notification
notification['status'] = 'active'

# Send notification
notification['status'] = 'completed'
notification['completed_at'] = datetime.now().isoformat()

# Cancel notification
notification['status'] = 'cancelled'
notification['cancelled_at'] = datetime.now().isoformat()
</code></code></pre><h2>Understanding Templates</h2><p><strong>What are templates?</strong></p><p>Templates are <strong>saved notification configurations</strong> you can reuse:</p><pre><code><code># Template
{
  "name": "Daily Standup",
  "type": "scheduled",
  "time": "09:30",
  "title": "Daily Standup",
  "message": "Morning team sync"
}

# Create from template
notification = create_from_template("Daily Standup")
# Automatically fills in all fields
</code></code></pre><p><strong>Template benefits:</strong></p><p>&#9989; <strong>Consistency</strong> - Same notification every time<br>&#9989; <strong>Speed</strong> - One click instead of typing<br>&#9989; <strong>Tracking</strong> - See which templates are most used<br>&#9989; <strong>Reusability</strong> - Use same reminder daily/weekly</p><p><strong>Common templates:</strong></p><ul><li><p>Daily standup (9:30 AM)</p></li><li><p>Lunch break (12:00 PM)</p></li><li><p>End of workday (5:00 PM)</p></li><li><p>Hydration reminder (every 1 hour)</p></li><li><p>Pomodoro session (25 minute timer)</p></li></ul><h2>Understanding Notification IDs</h2><p><strong>Why unique IDs?</strong></p><p>Need a way to identify specific notifications:</p><pre><code><code># Without IDs - ambiguous!
cancel_notification("Team Meeting")  # Which one?

# With IDs - precise!
cancel_notification("notif_001")  # Exact notification
</code></code></pre><p><strong>ID generation:</strong></p><pre><code><code>def generate_id(self):
    # Get highest existing ID number
    existing_ids = [n['id'] for n in self.notifications]
    
    if not existing_ids:
        return "notif_001"
    
    # Extract numbers and increment
    numbers = [int(id.split('_')[1]) for id in existing_ids]
    next_num = max(numbers) + 1
    
    return f"notif_{next_num:03d}"
</code></code></pre><h2>Understanding Multi-Notification Management</h2><p><strong>Managing multiple scheduled notifications:</strong></p><pre><code><code># Track all active threads
active_threads = []

# Start notification 1
thread1 = threading.Thread(target=send_later, args=("notif_001",))
thread1.start()
active_threads.append(thread1)

# Start notification 2
thread2 = threading.Thread(target=send_later, args=("notif_002",))
thread2.start()
active_threads.append(thread2)

# Both running simultaneously!
</code></code></pre><p><strong>Cancelling specific notifications:</strong></p><pre><code><code># Each thread has reference to notification ID
# Store stop flags in dictionary

stop_flags = {}

def send_later(notif_id):
    while not stop_flags.get(notif_id, False):
        # Wait and check if cancelled
        time.sleep(1)
    
    # Send notification
    send_notification(...)

# To cancel
stop_flags[notif_id] = True
</code></code></pre><h2>Understanding Auto-Reload on Startup</h2><p><strong>Resume notifications on startup:</strong></p><pre><code><code>def load_and_schedule(self):
    # Load from JSON
    data = self.load_data()
    
    # Find active scheduled notifications
    for notif in data['notifications']:
        if notif['status'] == 'active':
            if notif['type'] == 'scheduled':
                # Reschedule if time hasn't passed
                self._reschedule_notification(notif)
            elif notif['type'] == 'recurring':
                # Restart recurring reminder
                self._restart_recurring(notif)
</code></code></pre><p><strong>What gets rescheduled:</strong></p><p>&#9989; <strong>Scheduled notifications</strong> - If time is in future<br>&#9989; <strong>Recurring reminders</strong> - Always restart<br>&#10060; <strong>Completed notifications</strong> - Already sent<br>&#10060; <strong>Past scheduled times</strong> - Mark as missed</p><h2>What You&#8217;ve Accomplished This Week</h2><ul><li><p><strong>Day 1:</strong> Desktop notifications with cross-platform support</p></li><li><p><strong>Day 2:</strong> Scheduling, timers, and Pomodoro</p></li><li><p><strong>Day 3:</strong> Persistence, templates, and full management</p></li></ul><p><strong>You now have:</strong></p><p>&#9989; <strong>Cross-platform notifications</strong> - Works on Mac, Windows, Linux<br>&#9989; <strong>Flexible scheduling</strong> - Instant, scheduled, recurring, countdown<br>&#9989; <strong>Persistent storage</strong> - Survives program restarts<br>&#9989; <strong>Template system</strong> - Reusable notification configs<br>&#9989; <strong>Full management</strong> - Create, list, cancel, edit<br>&#9989; <strong>Production-ready</strong> - Clean code, error handling, JSON storage</p><p></p><h2>View Code Evolution</h2><p>Compare today&#8217;s full management system with earlier versions and see how we evolved from simple notifications &#8594; scheduling &#8594; complete persistence.</p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/desktop-notification-system-day-3">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Desktop Notification System: Day 2 - Scheduled Notifications & Timers]]></title><description><![CDATA[Learn Python by practicing every day with a new project.]]></description><link>https://dailypythonprojects.substack.com/p/desktop-notification-system-day-2</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/desktop-notification-system-day-2</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Wed, 06 May 2026 17:05:07 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/858994ac-a29b-4ec2-83ab-ca67c2b77fa4_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>Desktop Notification System</strong> that displays toast notifications, schedules reminders, and manages alerts &#8212; perfect for building productivity tools!</p><ul><li><p><strong>Day 1:</strong> Simple Desktop Notifications</p></li><li><p><strong>Day 2:</strong> Scheduled Notifications &amp; Timers <strong>(Today)</strong></p></li><li><p><strong>Day 3:</strong> Smart Notification Manager with Persistence</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-16">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>Yesterday we built basic notifications. Today we&#8217;re adding <strong>time-based features</strong> &#8212; schedule notifications for specific times, create countdown timers, and build a Pomodoro work timer!</p><p>You&#8217;ll learn scheduling, time calculations, threading, and building productivity tools that notify you at the right moment!</p><h2>Project Task</h2><p>Enhance the notification system with time-based features:</p><ul><li><p>Schedule notifications for specific times</p></li><li><p>Countdown timers with notifications</p></li><li><p>Recurring reminders (every X minutes)</p></li><li><p>Pomodoro timer (25 min work + 5 min break)</p></li><li><p>Time parsing (natural language like &#8220;5pm&#8221;, &#8220;30 minutes&#8221;)</p></li><li><p>Background task scheduling</p></li><li><p>Visual countdown display</p></li><li><p>All Day 1 features still work</p></li></ul><p>This project gives you hands-on practice with scheduling, datetime parsing, threading, timers, background tasks, and building productivity tools &#8212; essential skills for automation and time-based applications!</p><h2>Expected Output</h2><p><strong>Running the enhanced notification system:</strong></p><pre><code><code>python solution.py</code></code></pre><p><strong>Console Output:</strong></p><p>Here is what the user sees in the console when running the program:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0r82!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0r82!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png 424w, https://substackcdn.com/image/fetch/$s_!0r82!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png 848w, https://substackcdn.com/image/fetch/$s_!0r82!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png 1272w, https://substackcdn.com/image/fetch/$s_!0r82!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0r82!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:877874,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/196681939?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0r82!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png 424w, https://substackcdn.com/image/fetch/$s_!0r82!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png 848w, https://substackcdn.com/image/fetch/$s_!0r82!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png 1272w, https://substackcdn.com/image/fetch/$s_!0r82!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c14bfd0-0947-4dc8-809e-8612172f7e7e_2142x1428.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In the interaction above the user has chosen to use the Pomodoro feature. After 25 minutes have elapsed, the user will get a native desktop notification:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8vf3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8vf3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png 424w, https://substackcdn.com/image/fetch/$s_!8vf3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png 848w, https://substackcdn.com/image/fetch/$s_!8vf3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png 1272w, https://substackcdn.com/image/fetch/$s_!8vf3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8vf3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:358725,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/196681939?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8vf3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png 424w, https://substackcdn.com/image/fetch/$s_!8vf3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png 848w, https://substackcdn.com/image/fetch/$s_!8vf3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png 1272w, https://substackcdn.com/image/fetch/$s_!8vf3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f58e305-e004-472e-a72a-3f3a7705a6bd_1818x1212.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After some minutes, another focus session will start.</p><h2>Setup Instructions</h2><p><strong>No new installations needed!</strong></p><p>Same setup as Day 1:</p><p><strong>macOS:</strong></p><pre><code><code>python solution.py</code></code></pre><p><strong>Windows:</strong></p><pre><code><code>pip install win10toast  # If not already installed
python solution.py</code></code></pre><p><strong>Linux:</strong></p><pre><code><code>python solution.py</code></code></pre><h2>Understanding Time-Based Scheduling</h2><p><strong>Three types of scheduling:</strong></p><p><strong>1. Absolute time (schedule_at):</strong></p><pre><code><code># Notify at 3:00 PM today
scheduler.schedule_at("15:00", "Meeting", "Daily standup")
</code></code></pre><p><strong>2. Relative time (countdown):</strong></p><pre><code><code># Notify in 25 minutes
scheduler.countdown_timer(25, "Timer Done", "Break time!")
</code></code></pre><p><strong>3. Recurring (repeating):</strong></p><pre><code><code># Notify every 30 minutes
scheduler.recurring_reminder(30, "Reminder", "Drink water")
</code></code></pre><h2>Understanding Datetime Parsing</h2><p><strong>Converting user input to datetime objects:</strong></p><pre><code><code>from datetime import datetime, timedelta

# Parse "15:00" to today at 3 PM
def parse_time(time_str):
    # Split "15:00" into hours and minutes
    hours, minutes = map(int, time_str.split(':'))
    
    # Get today's date
    now = datetime.now()
    
    # Combine with specified time
    scheduled_time = now.replace(hour=hours, minute=minutes, second=0)
    
    # If time already passed today, schedule for tomorrow
    if scheduled_time &lt; now:
        scheduled_time += timedelta(days=1)
    
    return scheduled_time
</code></code></pre><p><strong>Calculating time until notification:</strong></p><pre><code><code># How long until 3 PM?
now = datetime.now()
scheduled_time = parse_time("15:00")

time_diff = scheduled_time - now

# Convert to readable format
hours = time_diff.seconds // 3600
minutes = (time_diff.seconds % 3600) // 60

print(f"Time until notification: {hours} hours {minutes} minutes")
</code></code></pre><h2>Understanding Threading for Background Tasks</h2><p><strong>Why threading?</strong></p><p>Without threading, your program <strong>blocks</strong> while waiting:</p><pre><code><code># BAD - Program freezes for 25 minutes!
time.sleep(25 * 60)
send_notification("Timer Done", "Break time!")
# User can't do anything else during wait
</code></code></pre><p><strong>With threading, program stays responsive:</strong></p><pre><code><code>import threading

def timer_thread(minutes):
    time.sleep(minutes * 60)
    send_notification("Timer Done", "Break time!")

# Start timer in background
thread = threading.Thread(target=timer_thread, args=(25,))
thread.start()

# Program continues - user can start another timer!
</code></code></pre><p><strong>Our approach:</strong></p><pre><code><code>def countdown_timer(self, minutes, title, message):
    # Create background thread
    thread = threading.Thread(
        target=self._run_countdown,
        args=(minutes, title, message)
    )
    thread.daemon = True  # Thread dies when main program exits
    thread.start()
</code></code></pre><h2>Understanding the Pomodoro Technique</h2><p><strong>What is Pomodoro?</strong></p><p>A productivity method that uses timed work sessions with breaks:</p><ol><li><p><strong>Work</strong> for 25 minutes (focused)</p></li><li><p><strong>Break</strong> for 5 minutes (short)</p></li><li><p><strong>Repeat</strong> 4 times</p></li><li><p><strong>Long break</strong> 15-30 minutes</p></li></ol><p><strong>Our implementation:</strong></p><pre><code><code>def start_pomodoro(self, sessions=4):
    for i in range(sessions):
        # Work session
        print(f"&#127813; Work Session {i+1}/{sessions}")
        self.countdown_timer(25, "Work Complete", "Time for a break!")
        
        # Short break (except after last session)
        if i &lt; sessions - 1:
            print("&#9749; Break Time")
            self.countdown_timer(5, "Break Over", "Back to work!")
</code></code></pre><p><strong>Why it works:</strong></p><ul><li><p>&#9989; Maintains focus with time limits</p></li><li><p>&#9989; Prevents burnout with regular breaks</p></li><li><p>&#9989; Creates rhythm and routine</p></li><li><p>&#9989; Notifications keep you on track</p></li></ul><h2>Understanding Countdown Display</h2><p><strong>Live countdown in terminal:</strong></p><pre><code><code>import time

def show_countdown(minutes):
    total_seconds = minutes * 60
    
    for remaining in range(total_seconds, 0, -60):
        mins = remaining // 60
        secs = remaining % 60
        
        # Display with carriage return (overwrites line)
        print(f"\r&#9201;&#65039;  Time remaining: {mins}:{secs:02d}", end='', flush=True)
        
        time.sleep(60)  # Wait 1 minute
    
    print("\n&#128276; Timer complete!")
</code></code></pre><p><strong>Key techniques:</strong></p><ul><li><p><code>\r</code> - Carriage return (moves cursor to start of line)</p></li><li><p><code>end=''</code> - Don&#8217;t print newline</p></li><li><p><code>flush=True</code> - Force immediate output</p></li><li><p>Result: Counter updates in place instead of printing new lines</p></li></ul><h2>Understanding Recurring Reminders</h2><p><strong>How recurring reminders work:</strong></p><pre><code><code>def recurring_reminder(self, interval_minutes, title, message):
    while True:
        # Wait for interval
        time.sleep(interval_minutes * 60)
        
        # Send reminder
        self.send_notification(title, message)
        
        # Calculate next reminder time
        next_time = datetime.now() + timedelta(minutes=interval_minutes)
        print(f"Next reminder: {next_time.strftime('%I:%M %p')}")
</code></code></pre><p><strong>Stopping recurring reminders:</strong></p><pre><code><code># Use Ctrl+C to stop
try:
    recurring_reminder(30, "Break", "Take a break!")
except KeyboardInterrupt:
    print("\nRecurring reminder stopped.")
</code></code></pre><h2>Practical Use Cases</h2><p><strong>1. Meeting reminders:</strong></p><pre><code><code># Remind 15 minutes before 2 PM meeting
scheduler.schedule_at("13:45", "Meeting Soon", "Team sync at 2 PM")
</code></code></pre><p><strong>2. Pomodoro work sessions:</strong></p><pre><code><code># Deep work with automatic breaks
scheduler.start_pomodoro(sessions=4)
</code></code></pre><p><strong>3. Hydration reminders:</strong></p><pre><code><code># Drink water every hour
scheduler.recurring_reminder(60, "Hydration", "Drink some water!")
</code></code></pre><p><strong>4. Cooking timers:</strong></p><pre><code><code># Pasta cooking timer
scheduler.countdown_timer(10, "Pasta Ready", "Time to drain the pasta!")
</code></code></pre><p><strong>5. Long-running script notifications:</strong></p><pre><code><code># Start training ML model
train_model()  # Takes 3 hours

# Schedule notification for when it should be done
scheduler.countdown_timer(180, "Training Complete", "Model training finished!")
</code></code></pre><h2>Coming Tomorrow</h2><p>Tomorrow we&#8217;re building a <strong>Smart Notification Manager</strong> with persistence &#8212; save your scheduled notifications, create notification templates, manage multiple reminders, and run everything from the system tray!</p><h2>View Code Evolution</h2><p>Compare today&#8217;s scheduler-enhanced system with yesterday&#8217;s basic notifications and see how we added time-based features.</p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/desktop-notification-system-day-2">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Desktop Notification System: Day 1 - Simple Desktop Notifications]]></title><description><![CDATA[Learn how to make a Python program that triggers native notifications on your computer.]]></description><link>https://dailypythonprojects.substack.com/p/desktop-notification-system-day-1</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/desktop-notification-system-day-1</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Tue, 05 May 2026 21:18:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ecd14843-c76e-4737-b6cf-c1ad09478555_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we build a <strong>Desktop Notification System</strong> that displays toast notifications, schedules reminders, and manages alerts &#8212; perfect for building productivity tools!</p><p><strong>Why build this?</strong> Because notifications are everywhere &#8212; apps, websites, productivity tools. You&#8217;ll learn how to create professional desktop alerts that grab attention at the right moment!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong>What you&#8217;ll learn:</strong> This series teaches you desktop notifications, system tray integration, scheduling, time-based triggers, persistence, and building tools that run in the background &#8212; essential skills for automation and productivity apps!</p><p><strong>Why users love this:</strong> Create reminders that actually work! No more forgetting tasks or missing deadlines. By Day 3, you&#8217;ll have a smart notification manager running silently in your system tray!</p><ul><li><p><strong>Day 1:</strong> Simple Desktop Notifications <strong>(Today)</strong></p></li><li><p><strong>Day 2:</strong> Scheduled Notifications &amp; Timers</p></li><li><p><strong>Day 3:</strong> Smart Notification Manager with Persistence</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-16">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>We&#8217;re starting with the foundation: a simple notification system that displays desktop toast notifications with custom messages, titles, and durations!</p><p>You&#8217;ll learn how to trigger system notifications from Python using native OS capabilities &#8212; no complex dependencies!</p><h2>Project Task</h2><p>Create a desktop notification system that:</p><ul><li><p>Displays toast notifications on Windows/Mac/Linux</p></li><li><p>Custom title and message</p></li><li><p>Adjustable duration (how long it shows)</p></li><li><p>Multiple notification types (info, warning, success, error)</p></li><li><p>Simple command-line interface</p></li><li><p>Cross-platform compatibility</p></li><li><p>Uses native OS notification systems</p></li><li><p>No complex dependencies</p></li></ul><p>This project gives you hands-on practice with desktop notifications, system integration, cross-platform development, user alerts, and building tools that interact with the operating system &#8212; essential for automation and productivity apps!</p><h2>Expected Output</h2><p><strong>Running the notification tool:</strong></p><pre><code><code>python solution.py</code></code></pre><p><strong>Console Output:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Xmxe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Xmxe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png 424w, https://substackcdn.com/image/fetch/$s_!Xmxe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png 848w, https://substackcdn.com/image/fetch/$s_!Xmxe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png 1272w, https://substackcdn.com/image/fetch/$s_!Xmxe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Xmxe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:640732,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/196590756?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Xmxe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png 424w, https://substackcdn.com/image/fetch/$s_!Xmxe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png 848w, https://substackcdn.com/image/fetch/$s_!Xmxe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png 1272w, https://substackcdn.com/image/fetch/$s_!Xmxe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf2d6bf-726f-45e1-9daf-d8a5f4c1e777_1668x1112.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong>That would trigger a notification on your computer:</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!slJU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!slJU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png 424w, https://substackcdn.com/image/fetch/$s_!slJU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png 848w, https://substackcdn.com/image/fetch/$s_!slJU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png 1272w, https://substackcdn.com/image/fetch/$s_!slJU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!slJU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png" width="1344" height="896" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:896,&quot;width&quot;:1344,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:540494,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/196590756?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!slJU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png 424w, https://substackcdn.com/image/fetch/$s_!slJU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png 848w, https://substackcdn.com/image/fetch/$s_!slJU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png 1272w, https://substackcdn.com/image/fetch/$s_!slJU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ff503f-178d-45f0-9bb5-858fee7e2fb9_1344x896.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><p><strong>macOS and Linux (no installation needed!)</strong></p><p><strong>Windows:</strong></p><pre><code><code>pip install win10toast

</code></code></pre><p><strong>Platform support:</strong></p><ul><li><p>&#9989; <strong>macOS</strong> - Native notification center via <code>osascript</code></p></li><li><p>&#9989; <strong>Windows 10/11</strong> - Native toast notifications via <code>win10toast</code></p></li><li><p>&#9989; <strong>Linux</strong> - Native notifications via <code>notify-send</code></p></li></ul><h2>Understanding Desktop Notifications</h2><p><strong>What are desktop notifications?</strong></p><p>Desktop notifications (also called &#8220;toast notifications&#8221;) are small popup messages that appear on your screen to alert you about events, updates, or reminders.</p><p><strong>Common uses:</strong></p><ul><li><p>Email notifications</p></li><li><p>Chat messages</p></li><li><p>Reminders and alarms</p></li><li><p>Task completions</p></li><li><p>System alerts</p></li><li><p>Download completions</p></li></ul><p><strong>How they work:</strong></p><pre><code><code>Your Python Script &#8594; Native OS API &#8594; Desktop Notification</code></code></pre><p><strong>Example flow:</strong></p><pre><code><code># 1. Your code
notifier.send_notification("Download Complete", "file.zip is ready!")

# 2. Platform detection
# macOS: Uses osascript
# Windows: Uses win10toast
# Linux: Uses notify-send

# 3. OS displays notification
# User sees popup on screen
</code></code></pre><h2>Understanding Cross-Platform Notifications</h2><p><strong>Why we detect the platform:</strong></p><p>Different operating systems have different notification systems. Our code automatically detects your OS and uses the right method!</p><p><strong>macOS approach:</strong></p><pre><code><code># Uses AppleScript via osascript command
osascript -e 'display notification "message" with title "title"'
</code></code></pre><p><strong>Windows approach:</strong></p><pre><code><code># Uses Windows Toast Notification API
from win10toast import ToastNotifier
toaster = ToastNotifier()
toaster.show_toast("Title", "Message")
</code></code></pre><p><strong>Linux approach:</strong></p><pre><code><code># Uses notify-send (part of libnotify)
notify-send "Title" "Message" -t 5000
</code></code></pre><p><strong>Our unified approach:</strong></p><pre><code><code>def send_notification(self, title, message, duration=5):
    if self.platform == "Darwin":  # macOS
        self._send_macos_notification(title, message)
    elif self.platform == "Windows":
        self._send_windows_notification(title, message, duration)
    elif self.platform == "Linux":
        self._send_linux_notification(title, message, duration)
</code></code></pre><p><strong>Benefits:</strong></p><ul><li><p>&#9989; Same code works everywhere</p></li><li><p>&#9989; No complex dependencies</p></li><li><p>&#9989; Uses native OS features</p></li><li><p>&#9989; Looks native on each platform</p></li></ul><h2>Understanding Notification Parameters</h2><p><strong>Title:</strong></p><ul><li><p>Short, attention-grabbing text</p></li><li><p>Appears in bold at the top</p></li><li><p>Keep under 50 characters</p></li><li><p>Example: &#8220;Download Complete&#8221;, &#8220;New Message&#8221;, &#8220;Reminder&#8221;</p></li></ul><p><strong>Message:</strong></p><ul><li><p>Main notification content</p></li><li><p>Can be longer (up to 200 characters recommended)</p></li><li><p>Should be clear and actionable</p></li><li><p>Example: &#8220;Your file download.zip is ready to use&#8221;</p></li></ul><p><strong>Duration:</strong></p><ul><li><p>How long notification stays on screen (seconds)</p></li><li><p>Default: 5-10 seconds</p></li><li><p>Short messages: 5 seconds</p></li><li><p>Important messages: 10+ seconds</p></li><li><p><strong>Note:</strong> macOS ignores duration and uses system default</p></li></ul><p><strong>App Name:</strong></p><ul><li><p>Identifies which app sent the notification</p></li><li><p>Appears at bottom of notification</p></li><li><p>Example: &#8220;Python Notifier&#8221;, &#8220;My Reminder App&#8221;</p></li></ul><h2>Understanding Notification Types</h2><p><strong>We implement different notification types with emoji indicators:</strong></p><p><strong>1. Info Notification (&#8505;&#65039;):</strong></p><pre><code><code>notifier.send_info(
    title="Information",
    message="This is an informational message"
)
# Displays: &#8505;&#65039; Information
</code></code></pre><p><strong>2. Success Notification (&#9989;):</strong></p><pre><code><code>notifier.send_success(
    title="Success!",
    message="Operation completed successfully"
)
# Displays: &#9989; Success!
</code></code></pre><p><strong>3. Urgent Notification (&#9888;&#65039;):</strong></p><pre><code><code>notifier.send_urgent(
    title="Warning",
    message="Action required!"
)
# Displays: &#9888;&#65039; Warning
</code></code></pre><p><strong>4. Error Notification (&#10060;):</strong></p><pre><code><code>notifier.send_error(
    title="Error",
    message="Something went wrong"
)
# Displays: &#10060; Error
</code></code></pre><p><strong>Why emojis?</strong></p><ul><li><p>&#9989; Universal visual indicators</p></li><li><p>&#9989; Work on all platforms</p></li><li><p>&#9989; No icon files needed</p></li><li><p>&#9989; Instant recognition</p></li></ul><h2>Practical Use Cases</h2><p><strong>1. Long-running scripts:</strong></p><pre><code><code># Scraping script
scraper.run()  # Takes 30 minutes

# Notify when done
notifier.send_success(
    "Scraping Complete",
    "Collected 1,500 quotes successfully!"
)
</code></code></pre><p><strong>2. File downloads:</strong></p><pre><code><code>download_file(url)

notifier.send_notification(
    "Download Complete",
    f"{filename} is ready!"
)
</code></code></pre><p><strong>3. Task reminders:</strong></p><pre><code><code>notifier.send_notification(
    "Break Time!",
    "You've been working for 1 hour. Take a 5-minute break."
)
</code></code></pre><p><strong>4. System monitoring:</strong></p><pre><code><code>if disk_space &lt; 10:
    notifier.send_urgent(
        "Low Disk Space",
        f"Only {disk_space}% remaining!"
    )
</code></code></pre><p><strong>5. Build/test completions:</strong></p><pre><code><code>run_tests()

if all_passed:
    notifier.send_success("Tests Passed", "All 47 tests passed!")
else:
    notifier.send_error("Tests Failed", f"{failed_count} tests failed")
</code></code></pre><h2>Coming Tomorrow</h2><p>Tomorrow we&#8217;re adding <strong>scheduled notifications and timers</strong> &#8212; set reminders for specific times, create countdown timers, and build a Pomodoro-style work timer with automatic notifications!</p><h2>Skeleton and Solution</h2><p>Below you will find both a downloadable skeleton.py file to help you code the project with comment guides and the downloadable solution.py file containing the correct solution.</p><p>Get the code skeleton here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/view/ZkSgSLoa8GnkZbeONH4y9A&quot;,&quot;text&quot;:&quot;View Code Skeleton&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/view/ZkSgSLoa8GnkZbeONH4y9A"><span>View Code Skeleton</span></a></p><p></p><p>Get the code solution here: </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/evolution/bGbd5o7Y3Bfzqr4cGhkV-w&quot;,&quot;text&quot;:&quot;View Code Solution&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/evolution/bGbd5o7Y3Bfzqr4cGhkV-w"><span>View Code Solution</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Build a Quote Scraper: Day 3 - Adding Inheritance & Multiple Scrapers ]]></title><description><![CDATA[Today, we go more advanced with classes and use class inheritance in the code. We also add multiple scrapers.]]></description><link>https://dailypythonprojects.substack.com/p/build-a-quote-scraper-day-3-adding</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-quote-scraper-day-3-adding</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Thu, 30 Apr 2026 13:03:21 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c6cc07ea-906e-4051-b98a-87ca9c6a707f_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we learn <strong>Object-Oriented Programming (OOP)</strong> by building a web scraper that evolves from functions to classes to inheritance.</p><ul><li><p><strong>Day 1:</strong> Quote Scraper with Functions</p></li><li><p><strong>Day 2:</strong> Refactoring to Classes</p></li><li><p><strong>Day 3:</strong> Adding Inheritance &amp; Multiple Scrapers <strong>(Today)</strong></p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-15">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p><strong>Welcome to the finale!</strong> Yesterday we refactored to classes. Today we&#8217;re taking it further with <strong>inheritance</strong> &#8212; creating a base <code>Scraper</code> class and two specialized scrapers that inherit from it!</p><p>You&#8217;ll build <code>QuoteScraper</code> and <code>AuthorScraper</code> &#8212; both share common scraping logic but extract different data. This is how professional code reuses functionality!</p><h2>Project Task</h2><p>Create a scraping system with inheritance that:</p><ul><li><p>Base <code>Scraper</code> class with common scraping logic</p></li><li><p><code>QuoteScraper</code> inherits from <code>Scraper</code> (extracts quotes)</p></li><li><p><code>AuthorScraper</code> inherits from <code>Scraper</code> (extracts authors)</p></li><li><p>Shared methods in base class (fetch, scrape, save)</p></li><li><p>Specialized parsing in each subclass</p></li><li><p>Both scrapers work independently</p></li><li><p>Demonstrates clean code reuse</p></li></ul><p>This project gives you hands-on practice with inheritance, base classes, method overriding, code reuse, and OOP design &#8212; essential skills for building maintainable applications!</p><h2>Expected Output</h2><p><strong>Running the scraper:</strong></p><pre><code><code>python solution.py</code></code></pre><p><strong>Console Output:</strong></p><p>The scraper scrapes quotes like in Day 2 but in addition, it also scrapes other data such as authors:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yEmw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yEmw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png 424w, https://substackcdn.com/image/fetch/$s_!yEmw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png 848w, https://substackcdn.com/image/fetch/$s_!yEmw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png 1272w, https://substackcdn.com/image/fetch/$s_!yEmw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yEmw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1278239,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/195742235?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yEmw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png 424w, https://substackcdn.com/image/fetch/$s_!yEmw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png 848w, https://substackcdn.com/image/fetch/$s_!yEmw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png 1272w, https://substackcdn.com/image/fetch/$s_!yEmw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2456fefd-abaf-415d-9665-e1d23557f747_2640x1760.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install requests beautifulsoup4</code></code></pre><p><strong>Run the scrit:</strong></p><pre><code><code>python solution.py</code></code></pre><h2>Understanding Inheritance</h2><p><strong>What is inheritance?</strong></p><p>Inheritance lets you create a <strong>base class</strong> with common functionality, then create <strong>child classes</strong> that inherit and extend that functionality.</p><p><strong>Our hierarchy:</strong></p><pre><code><code>        Scraper (Base Class)
           &#8593;
           |
    &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
    |             |
QuoteScraper  AuthorScraper
</code></code></pre><p><strong>Before inheritance (repetitive):</strong></p><pre><code><code>class QuoteScraper:
    def fetch_page(self, url):
        # Fetching logic
        pass
    
    def scrape(self):
        # Pagination logic
        pass
    
    def parse_quotes(self, html):
        # Different!
        pass

class AuthorScraper:
    def fetch_page(self, url):
        # DUPLICATE CODE!
        pass
    
    def scrape(self):
        # DUPLICATE CODE!
        pass
    
    def parse_authors(self, html):
        # Different!
        pass
</code></code></pre><p><strong>After inheritance (clean):</strong></p><pre><code><code>class Scraper:
    """Base class - write once, use everywhere"""
    def fetch_page(self, url):
        # Common code
        pass
    
    def scrape(self):
        # Common code
        pass

class QuoteScraper(Scraper):
    """Inherits fetch_page and scrape"""
    def parse_data(self, html):
        # Only write the unique part
        pass

class AuthorScraper(Scraper):
    """Also inherits fetch_page and scrape"""
    def parse_data(self, html):
        # Only write the unique part
        pass
</code></code></pre><h2>Understanding the Base Scraper Class</h2><p><strong>The base class contains what&#8217;s shared:</strong></p><pre><code><code>class Scraper:
    """Base scraper with common functionality"""
    
    def __init__(self, base_url, max_pages=3):
        self.base_url = base_url
        self.max_pages = max_pages
        self.data = []
    
    def fetch_page(self, url):
        """Fetch HTML - same for all scrapers"""
        response = requests.get(url)
        return response.text
    
    def scrape(self):
        """Scrape pages - same logic for all scrapers"""
        for page_num in range(1, self.max_pages + 1):
            url = f"{self.base_url}/page/{page_num}/"
            html = self.fetch_page(url)
            
            # Call parse_data (implemented by child)
            data = self.parse_data(html)
            
            self.data.extend(data)
    
    def save_to_json(self, filename):
        """Save data - same for all scrapers"""
        with open(filename, 'w') as f:
            json.dump(self.data, f)
    
    def parse_data(self, html):
        """Must be implemented by child classes"""
        raise NotImplementedError("Child must implement parse_data()")
</code></code></pre><p><strong>Key points:</strong></p><p>&#9989; <code>fetch_page()</code> - All scrapers fetch HTML the same way<br>&#9989; <code>scrape()</code> - All scrapers loop through pages the same way<br>&#9989; <code>save_to_json()</code> - All scrapers save files the same way<br>&#9989; <code>parse_data()</code> - Each child implements differently</p><h2>Understanding Child Classes</h2><p><strong>QuoteScraper - extracts quotes:</strong></p><pre><code><code>class QuoteScraper(Scraper):
    """Inherits from Scraper"""
    
    def __init__(self, base_url, tags_filter, max_pages=3):
        super().__init__(base_url, max_pages)  # Call parent init
        self.tags_filter = tags_filter
    
    def parse_data(self, html):
        """Parse quotes - unique to this scraper"""
        soup = BeautifulSoup(html, 'html.parser')
        quotes = soup.find_all('div', class_='quote')
        
        results = []
        for quote in quotes:
            text = quote.find('span', class_='text').text
            author = quote.find('small', class_='author').text
            tags = [tag.text for tag in quote.find_all('a', class_='tag')]
            
            # Filter by tags
            if any(tag in self.tags_filter for tag in tags):
                results.append({
                    'text': text,
                    'author': author,
                    'tags': tags
                })
        
        return results
</code></code></pre><p><strong>AuthorScraper - counts authors:</strong></p><pre><code><code>class AuthorScraper(Scraper):
    """Also inherits from Scraper"""
    
    def parse_data(self, html):
        """Parse authors - unique to this scraper"""
        soup = BeautifulSoup(html, 'html.parser')
        authors = soup.find_all('small', class_='author')
        
        # Just return author names
        return [author.text for author in authors]
</code></code></pre><p><strong>What they inherit:</strong></p><ul><li><p>&#9989; <code>fetch_page()</code> from <code>Scraper</code></p></li><li><p>&#9989; <code>scrape()</code> from <code>Scraper</code></p></li><li><p>&#9989; <code>save_to_json()</code> from <code>Scraper</code></p></li></ul><p><strong>What they implement:</strong></p><ul><li><p>&#127381; <code>parse_data()</code> - unique to each scraper</p></li></ul><h2>Understanding <code>super()</code></h2><p><code>super()</code><strong> calls the parent class:</strong></p><pre><code><code>class QuoteScraper(Scraper):
    def __init__(self, base_url, tags_filter, max_pages=3):
        # Call parent's __init__ first
        super().__init__(base_url, max_pages)
        
        # Then add child-specific attributes
        self.tags_filter = tags_filter
</code></code></pre><p><strong>What happens:</strong></p><pre><code><code>scraper = QuoteScraper("http://example.com", ['life'], max_pages=3)

# Step 1: super().__init__(base_url, max_pages)
#   &#8594; self.base_url = "http://example.com"
#   &#8594; self.max_pages = 3
#   &#8594; self.data = []

# Step 2: Child __init__ continues
#   &#8594; self.tags_filter = ['life']

# Result: All attributes set!
</code></code></pre><h2>Why Inheritance Matters</h2><p><strong>What we gain:</strong></p><p><strong>1. Write once, use everywhere:</strong></p><pre><code><code># fetch_page() written ONCE in Scraper
# Both QuoteScraper and AuthorScraper use it
</code></code></pre><p><strong>2. Easy to add new scrapers:</strong></p><pre><code><code>class TagScraper(Scraper):
    def parse_data(self, html):
        # Only implement the unique part!
        # Everything else inherited
        pass
</code></code></pre><p><strong>3. Fix bugs in one place:</strong></p><pre><code><code># Bug in fetch_page()?
# Fix it in Scraper class
# All children benefit automatically!
</code></code></pre><p><strong>4. Consistent behavior:</strong></p><pre><code><code># All scrapers fetch pages the same way
# All scrapers handle pagination the same way
# All scrapers save files the same way
</code></code></pre><h2>Code Comparison: Day 2 vs Day 3</h2><p><strong>Day 2 (Single QuoteScraper class):</strong></p><ul><li><p>&#9989; Better than functions</p></li><li><p>&#10060; Can&#8217;t reuse for other scrapers</p></li><li><p>&#10060; Would duplicate code for AuthorScraper</p></li></ul><p><strong>Day 3 (Inheritance):</strong></p><ul><li><p>&#9989; Base class holds common code</p></li><li><p>&#9989; Easy to add new scrapers</p></li><li><p>&#9989; No code duplication</p></li><li><p>&#9989; Fix once, benefit everywhere</p></li></ul><h2>What You&#8217;ve Accomplished This Week</h2><p>&#127881; <strong>Congratulations!</strong> You&#8217;ve completed the <strong>OOP journey</strong>!</p><ul><li><p><strong>Day 1:</strong> Functions - Simple and procedural</p></li><li><p><strong>Day 2:</strong> Classes - Organized with encapsulation</p></li><li><p><strong>Day 3:</strong> Inheritance - Reusable and scalable</p></li></ul><p><strong>You now understand:</strong></p><p>&#9989; <strong>Functions</strong> &#8594; Small, independent tasks<br>&#9989; <strong>Classes</strong> &#8594; Grouping related data and behavior<br>&#9989; <strong>Inheritance</strong> &#8594; Sharing code across similar classes</p><p><strong>This is how professional Python is written!</strong></p><p><strong>Real-world examples of inheritance:</strong></p><ul><li><p><strong>Games:</strong> <code>Character</code> &#8594; <code>Player</code>, <code>Enemy</code>, <code>NPC</code></p></li><li><p><strong>Web frameworks:</strong> <code>View</code> &#8594; <code>ListView</code>, <code>DetailView</code>, <code>CreateView</code></p></li><li><p><strong>ML models:</strong> <code>Model</code> &#8594; <code>LinearRegression</code>, <code>DecisionTree</code>, <code>NeuralNetwork</code></p></li><li><p><strong>Data processing:</strong> <code>FileReader</code> &#8594; <code>CSVReader</code>, <code>JSONReader</code>, <code>XMLReader</code></p></li></ul><p><strong>You&#8217;ve mastered the fundamentals of OOP!</strong> &#128640;</p><h2>View Code Evolution</h2><p>Compare today&#8217;s inheritance-based system with earlier versions and see how we evolved from functions &#8594; classes &#8594; inheritance for maximum code reuse.</p><p></p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/build-a-quote-scraper-day-3-adding">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build a Quote Scraper: Day 2 - Refactoring to Classes]]></title><description><![CDATA[Today, our program behavior does not change, but we refactor the code making it more organized with classes instead of functions.]]></description><link>https://dailypythonprojects.substack.com/p/build-a-quote-scraper-day-2-refactoring</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-quote-scraper-day-2-refactoring</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Wed, 29 Apr 2026 12:17:53 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/91f71156-9441-4338-aacc-4771bf53fb4a_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we learn <strong>Object-Oriented Programming (OOP)</strong> by building a web scraper that evolves from functions to classes to inheritance.</p><ul><li><p><strong>Day 1:</strong> Quote Scraper with Functions</p></li><li><p><strong>Day 2:</strong> Refactoring to Classes <strong>(Today)</strong></p></li><li><p><strong>Day 3:</strong> Adding Inheritance &amp; Multiple Scrapers</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-15">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>Yesterday we built a quote scraper using <strong>only functions</strong>. Today we&#8217;re <strong>refactoring the exact same scraper using classes</strong>!</p><p>You&#8217;ll see the same functionality, but organized as a <code>QuoteScraper</code> class. This is your &#8220;aha moment&#8221; for understanding <em>why</em> and <em>when</em> to use classes instead of functions!</p><h2>Project Task</h2><p>Refactor yesterday&#8217;s scraper into a class-based design that:</p><ul><li><p>Same functionality as Day 1 (scrape, filter, save)</p></li><li><p>Organized as a <code>QuoteScraper</code> class</p></li><li><p>Instance variables store configuration and state</p></li><li><p>Methods replace standalone functions</p></li><li><p>Cleaner data flow with <code>self</code></p></li><li><p>Easier to extend and maintain</p></li><li><p>Same output as Day 1</p></li></ul><p>This project gives you hands-on practice with class design, instance variables, methods, <code>self</code> keyword, refactoring, and object-oriented thinking &#8212; the foundation of professional Python code!</p><h2>Expected Output</h2><p><strong>Running the scraper:</strong></p><pre><code><code>python solution.py
</code></code></pre><p><strong>Console Output:</strong></p><p>The output is the same as the output of <a href="https://dailypythonprojects.substack.com/p/build-a-job-listing-scraper-day-1">Day 1</a>. We get the quotes displayed. So, the behavior of the program does not change. It is only the code that changes:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ARjo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ARjo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png 424w, https://substackcdn.com/image/fetch/$s_!ARjo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png 848w, https://substackcdn.com/image/fetch/$s_!ARjo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png 1272w, https://substackcdn.com/image/fetch/$s_!ARjo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ARjo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1372961,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/195741311?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ARjo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png 424w, https://substackcdn.com/image/fetch/$s_!ARjo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png 848w, https://substackcdn.com/image/fetch/$s_!ARjo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png 1272w, https://substackcdn.com/image/fetch/$s_!ARjo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f5dcc89-0503-461f-a322-7bcadc941d1f_2442x1628.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><br></p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install requests beautifulsoup4
</code></code></pre><p><strong>Same packages as Day 1!</strong></p><p><strong>Run the scraper:</strong></p><pre><code><code>python solution.py
</code></code></pre><p><strong>Same output as Day 1</strong>, but now it&#8217;s organized as a class!</p><h2>Understanding Classes vs Functions</h2><p><strong>Yesterday (Functions):</strong></p><pre><code><code># Pass data between functions
html = fetch_page(url)
quotes = parse_quotes(html)
filtered = filter_by_tags(quotes, tags_filter)
save_to_json(filtered, tags_filter)
</code></code></pre><p><strong>Problems:</strong></p><ul><li><p>&#10060; Pass <code>tags_filter</code> to every function</p></li><li><p>&#10060; No shared state</p></li><li><p>&#10060; Hard to track what data belongs together</p></li><li><p>&#10060; Difficult to extend</p></li></ul><p><strong>Today (Classes):</strong></p><pre><code><code># Create instance with configuration
scraper = QuoteScraper(base_url=url, tags_filter=tags)

# Call methods - they share state via self
scraper.scrape()  # Uses self.base_url and self.tags_filter
scraper.save_to_json()  # Uses self.quotes
</code></code></pre><p><strong>Benefits:</strong></p><ul><li><p>&#9989; Configuration stored once in <code>self</code></p></li><li><p>&#9989; Shared state across methods</p></li><li><p>&#9989; Related data grouped together</p></li><li><p>&#9989; Easy to extend with new methods</p></li></ul><h2>Understanding the QuoteScraper Class</h2><p><strong>Class structure:</strong></p><pre><code><code>class QuoteScraper:
    def __init__(self, base_url, tags_filter):
        """Initialize the scraper with configuration"""
        self.base_url = base_url
        self.tags_filter = tags_filter
        self.quotes = []  # Store results here
    
    def fetch_page(self, url):
        """Fetch HTML from URL"""
        # Same logic as Day 1 function
        return html
    
    def parse_quotes(self, html):
        """Parse quotes from HTML"""
        # Same logic as Day 1 function
        return quotes
    
    def filter_by_tags(self, quotes):
        """Filter quotes by self.tags_filter"""
        # Uses self.tags_filter instead of parameter
        return filtered
    
    def scrape(self):
        """Main scraping method"""
        page = 1
        while True:
            url = f"{self.base_url}/page/{page}/"
            html = self.fetch_page(url)
            quotes = self.parse_quotes(html)
            filtered = self.filter_by_tags(quotes)
            self.quotes.extend(filtered)  # Store in self.quotes
            page += 1
    
    def save_to_json(self):
        """Save self.quotes to JSON"""
        # Uses self.quotes and self.tags_filter
        pass
</code></code></pre><p></p><h2>Understanding <code>self</code></h2><p><strong>What is </strong><code>self</code><strong>?</strong></p><p><code>self</code> refers to the <strong>instance</strong> of the class. It&#8217;s how methods access the instance&#8217;s data.</p><pre><code><code># Create an instance
scraper = QuoteScraper(
    base_url="http://quotes.toscrape.com",
    tags_filter=['life']
)

# When you call a method:
scraper.fetch_page(url)

# Python automatically passes the instance as 'self':
QuoteScraper.fetch_page(scraper, url)
#                       ^^^^^^
#                       This is 'self'!
</code></code></pre><p><strong>Using self to share data:</strong></p><pre><code><code>class QuoteScraper:
    def __init__(self, tags_filter):
        self.tags_filter = tags_filter  # Store in instance
        self.quotes = []  # Shared across methods
    
    def scrape(self):
        # Access instance variables with self
        print(f"Filtering by: {self.tags_filter}")
        
        quotes = self.parse_quotes(html)
        filtered = self.filter_by_tags(quotes)
        
        # Store in instance variable
        self.quotes.extend(filtered)
    
    def filter_by_tags(self, quotes):
        # Use self.tags_filter instead of parameter!
        return [q for q in quotes if any(tag in self.tags_filter for tag in q['tags'])]
    
    def save_to_json(self):
        # Access self.quotes that scrape() filled
        data = {'quotes': self.quotes}
        # ...
</code></code></pre><p><strong>Without </strong><code>self</code><strong> (doesn&#8217;t work):</strong></p><pre><code><code>class QuoteScraper:
    def __init__(self, tags_filter):
        tags_filter = tags_filter  # &#10060; Local variable, lost after __init__
    
    def scrape(self):
        print(tags_filter)  # &#10060; ERROR: tags_filter not defined!
</code></code></pre><h2>Understanding Instance Variables</h2><p><strong>Instance variables</strong> = data that belongs to a specific instance.</p><p><strong>Example:</strong></p><pre><code><code># Create two different scrapers
scraper1 = QuoteScraper(tags_filter=['life'])
scraper2 = QuoteScraper(tags_filter=['humor'])

# Each has its own data!
print(scraper1.tags_filter)  # ['life']
print(scraper2.tags_filter)  # ['humor']

# Run both
scraper1.scrape()
scraper2.scrape()

# Each stores different quotes
print(len(scraper1.quotes))  # 20 life quotes
print(len(scraper2.quotes))  # 15 humor quotes
</code></code></pre><p><strong>Common instance variables in our scraper:</strong></p><pre><code><code>def __init__(self, base_url, tags_filter):
    # Configuration (set once, used everywhere)
    self.base_url = base_url
    self.tags_filter = tags_filter
    
    # State (changes during execution)
    self.quotes = []  # Filled by scrape()
    self.current_page = 1  # Tracks pagination
</code></code></pre><h2>Understanding Methods</h2><p><strong>Methods</strong> = functions that belong to a class.</p><p><strong>Two types of methods in our scraper:</strong></p><p><strong>1. Public methods</strong> (meant to be called from outside):</p><pre><code><code>class QuoteScraper:
    def scrape(self):
        """Main method - users call this"""
        # ...
    
    def save_to_json(self):
        """Save results - users call this"""
        # ...
</code></code></pre><p><strong>2. Helper methods</strong> (used internally):</p><pre><code><code>class QuoteScraper:
    def _fetch_page(self, url):
        """Internal helper - users don't call this directly"""
        # Underscore prefix = "private" by convention
        # ...
    
    def _parse_quotes(self, html):
        """Internal helper"""
        # ...
</code></code></pre><p><strong>Calling methods:</strong></p><pre><code><code># From outside the class
scraper = QuoteScraper(...)
scraper.scrape()  # Public method

# From inside the class (in another method)
class QuoteScraper:
    def scrape(self):
        html = self._fetch_page(url)  # Call helper method
        quotes = self._parse_quotes(html)  # Call another helper
</code></code></pre><h2>Why Classes Are Better Here</h2><p><strong>Day 1 approach (functions):</strong></p><pre><code><code># Problem: Pass everything everywhere
def main():
    tags = ['life', 'love']
    
    quotes = scrape_all_pages(BASE_URL, tags)
    filtered = filter_by_tags(quotes, tags)
    save_to_json(filtered, tags)
    
    # Want to scrape again with different tags?
    # Start over, pass everything again
    quotes2 = scrape_all_pages(BASE_URL, ['humor'])
    filtered2 = filter_by_tags(quotes2, ['humor'])
    save_to_json(filtered2, ['humor'])
</code></code></pre><p><strong>Day 2 approach (classes):</strong></p><pre><code><code># Solution: Create reusable objects
scraper1 = QuoteScraper(BASE_URL, ['life', 'love'])
scraper1.scrape()
scraper1.save_to_json()

scraper2 = QuoteScraper(BASE_URL, ['humor'])
scraper2.scrape()
scraper2.save_to_json()

# Easy to compare results
print(f"Life quotes: {len(scraper1.quotes)}")
print(f"Humor quotes: {len(scraper2.quotes)}")
</code></code></pre><p><strong>Other benefits:</strong></p><p>&#9989; <strong>Encapsulation</strong> - Related data and functions grouped together<br>&#9989; <strong>State management</strong> - <code>self.quotes</code> persists across method calls<br>&#9989; <strong>Reusability</strong> - Create multiple scraper instances<br>&#9989; <strong>Extensibility</strong> - Easy to add new methods tomorrow<br>&#9989; <strong>Clarity</strong> - Clear what data belongs to what</p><h2>Coming Tomorrow</h2><p>Tomorrow we&#8217;re adding <strong>inheritance</strong>! We&#8217;ll create a base <code>Scraper</code> class and inherit from it to create different scrapers (quotes, authors, tags). You&#8217;ll see how OOP makes it easy to reuse code across similar classes!</p><h2>View Code Evolution</h2><p>Compare today&#8217;s class-based solution with yesterday&#8217;s function-based version and see the differences in organization and structure.</p>
      <p>
          <a href="https://dailypythonprojects.substack.com/p/build-a-quote-scraper-day-2-refactoring">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build a Quote Scraper: Day 1 - Quote Scraper with Functions ]]></title><description><![CDATA[We start this project today by using Python functions and then in day 2 we refactor the code using classes for better code organization.]]></description><link>https://dailypythonprojects.substack.com/p/build-a-job-listing-scraper-day-1</link><guid isPermaLink="false">https://dailypythonprojects.substack.com/p/build-a-job-listing-scraper-day-1</guid><dc:creator><![CDATA[Ardit Sulce]]></dc:creator><pubDate>Tue, 28 Apr 2026 12:03:15 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f3a1ef24-8214-4ad9-ac13-4f659a8d0701_1160x785.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Projects in this week&#8217;s series:</h2><p>This week, we learn <strong>Object-Oriented Programming (OOP)</strong> by building a web scraper that evolves from functions to classes to inheritance.</p><p><strong>Why build this?</strong> Because OOP is how professional code is structured. You&#8217;ll see the same scraper written three ways &#8212; first with functions (today), then refactored to classes (Day 2), then enhanced with inheritance (Day 3). By the end, you&#8217;ll understand <em>why</em> classes matter!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong>What you&#8217;ll learn:</strong> This series teaches you function-based programming, class structures, object-oriented design, inheritance, code organization, and refactoring &#8212; essential skills for writing maintainable, professional Python code.</p><p><strong>Why users love this:</strong> You&#8217;ll see the same project evolve from simple functions to elegant OOP. Perfect for understanding when and why to use classes instead of functions!</p><ul><li><p><strong>Day 1:</strong> Quote Scraper with Functions <strong>(Today)</strong></p></li><li><p><strong>Day 2:</strong> Refactoring to Classes</p></li><li><p><strong>Day 3:</strong> Adding Inheritance &amp; Multiple Scrapers</p></li></ul><p><a href="https://dailypythonprojects.substack.com/t/week-15">View All Projects This Week</a></p><h2>Today&#8217;s Project</h2><p>We&#8217;re building a <strong>quote scraper</strong> that fetches quotes from <a href="http://quotes.toscrape.com">quotes.toscrape.com</a>, filters them by tag, and saves them to a JSON file. Today&#8217;s version uses <strong>pure functions</strong> &#8212; clean, simple, and procedural!</p><p>You&#8217;ll learn web scraping basics, HTML parsing, data filtering, and JSON storage!</p><h2>Project Task</h2><p>Create a quote scraper that:</p><ul><li><p>Scrapes quotes from quotes.toscrape.com</p></li><li><p>Extracts quote text, author, and tags</p></li><li><p>Filters quotes by specific tags (motivational, life, inspirational)</p></li><li><p>Handles multiple pages of quotes</p></li><li><p>Saves results to JSON file</p></li><li><p>Clean console output showing progress</p></li><li><p>Error handling for failed requests</p></li><li><p>Uses only functions (no classes yet!)</p></li></ul><p>This project gives you hands-on practice with web scraping, BeautifulSoup, requests library, data parsing, filtering, file I/O, and functional programming &#8212; all the foundations you need before learning OOP!</p><h2>Expected Output</h2><p><strong>Running the scraper:</strong></p><pre><code><code>python solution.py</code></code></pre><p><strong>Console Output:</strong></p><p>Running the program will print out the progress of the scraper as below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!25q6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!25q6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png 424w, https://substackcdn.com/image/fetch/$s_!25q6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png 848w, https://substackcdn.com/image/fetch/$s_!25q6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png 1272w, https://substackcdn.com/image/fetch/$s_!25q6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!25q6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1340697,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/195739925?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!25q6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png 424w, https://substackcdn.com/image/fetch/$s_!25q6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png 848w, https://substackcdn.com/image/fetch/$s_!25q6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png 1272w, https://substackcdn.com/image/fetch/$s_!25q6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c414750-4cdc-471e-a64f-ab7d77e9b8be_2616x1744.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Below that you will see the quotes scraped and printed out:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tTHL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tTHL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png 424w, https://substackcdn.com/image/fetch/$s_!tTHL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png 848w, https://substackcdn.com/image/fetch/$s_!tTHL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png 1272w, https://substackcdn.com/image/fetch/$s_!tTHL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tTHL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1459572,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/195739925?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tTHL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png 424w, https://substackcdn.com/image/fetch/$s_!tTHL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png 848w, https://substackcdn.com/image/fetch/$s_!tTHL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png 1272w, https://substackcdn.com/image/fetch/$s_!tTHL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb870b06b-df11-4d0e-91f2-9a055d4f3246_2502x1668.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong>Generated JSON file: </strong><code>quotes_motivational_life_inspirational.json</code></p><p><code>In addition to printing out the scraped content, the program will also save the data in a JSON file:</code></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xc6d!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xc6d!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png 424w, https://substackcdn.com/image/fetch/$s_!xc6d!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png 848w, https://substackcdn.com/image/fetch/$s_!xc6d!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png 1272w, https://substackcdn.com/image/fetch/$s_!xc6d!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xc6d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/faebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:994805,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://dailypythonprojects.substack.com/i/195739925?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xc6d!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png 424w, https://substackcdn.com/image/fetch/$s_!xc6d!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png 848w, https://substackcdn.com/image/fetch/$s_!xc6d!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png 1272w, https://substackcdn.com/image/fetch/$s_!xc6d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaebe6ec-a27f-4dd7-9226-5d5c39a3f848_2268x1512.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Setup Instructions</h2><p><strong>Install Required Packages:</strong></p><pre><code><code>pip install requests beautifulsoup4
</code></code></pre><p><strong>What each package does:</strong></p><ul><li><p><code>requests</code> - Makes HTTP requests to fetch web pages</p></li><li><p><code>beautifulsoup4</code> - Parses HTML and extracts data</p></li></ul><p><strong>Run the scraper:</strong></p><pre><code><code>python solution.py
</code></code></pre><p><strong>The scraper will:</strong></p><ol><li><p>Fetch quotes from quotes.toscrape.com</p></li><li><p>Filter by tags: motivational, life, inspirational</p></li><li><p>Save results to JSON file</p></li><li><p>Show progress in console</p></li></ol><p><strong>You can modify the tags</strong> in the script to scrape different quotes!</p><h2>Understanding Web Scraping Basics</h2><p><strong>What is web scraping?</strong></p><p>Web scraping is extracting data from websites by:</p><ol><li><p>Sending HTTP request to get the HTML</p></li><li><p>Parsing the HTML to find specific elements</p></li><li><p>Extracting the data you need</p></li><li><p>Storing it in a structured format</p></li></ol><p><strong>The scraping workflow:</strong></p><pre><code><code>URL &#8594; HTTP Request &#8594; HTML Response &#8594; Parse HTML &#8594; Extract Data &#8594; Save to File
</code></code></pre><p><strong>Example HTML structure from quotes.toscrape.com:</strong></p><pre><code><code>&lt;div class="quote"&gt;
  &lt;span class="text"&gt;"The world as we have created it..."&lt;/span&gt;
  &lt;small class="author"&gt;Albert Einstein&lt;/small&gt;
  &lt;div class="tags"&gt;
    &lt;a class="tag"&gt;change&lt;/a&gt;
    &lt;a class="tag"&gt;inspirational&lt;/a&gt;
    &lt;a class="tag"&gt;life&lt;/a&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></code></pre><p><strong>How we extract it:</strong></p><pre><code><code>import requests
from bs4 import BeautifulSoup

# 1. Fetch the page
response = requests.get('http://quotes.toscrape.com/page/1/')
html = response.text

# 2. Parse HTML
soup = BeautifulSoup(html, 'html.parser')

# 3. Find all quote divs
quotes = soup.find_all('div', class_='quote')

# 4. Extract data from each quote
for quote in quotes:
    text = quote.find('span', class_='text').text
    author = quote.find('small', class_='author').text
    tags = [tag.text for tag in quote.find_all('a', class_='tag')]
</code></code></pre><p><strong>Why quotes.toscrape.com?</strong></p><p>This website is specifically designed for learning web scraping:</p><ul><li><p>&#9989; Simple, clean HTML structure</p></li><li><p>&#9989; No anti-scraping measures</p></li><li><p>&#9989; Paginated data (multiple pages)</p></li><li><p>&#9989; Different data types (text, authors, tags)</p></li><li><p>&#9989; Legal and ethical to scrape</p></li></ul><h2>Understanding BeautifulSoup</h2><p><strong>BeautifulSoup</strong> is a Python library that makes HTML parsing easy.</p><p><strong>Key methods we use:</strong></p><pre><code><code>from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'html.parser')

# Find first matching element
quote = soup.find('div', class_='quote')

# Find all matching elements
quotes = soup.find_all('div', class_='quote')

# Get text content
text = element.text

# Get attribute value
href = element.get('href')
</code></code></pre><p><strong>CSS class selector:</strong></p><pre><code><code># Find by class name
soup.find('div', class_='quote')

# Find by ID
soup.find('div', id='my-id')

# Find by tag name
soup.find('span')
</code></code></pre><p><strong>Navigating the HTML tree:</strong></p><pre><code><code># Find nested elements
quote = soup.find('div', class_='quote')
author = quote.find('small', class_='author')

# Find multiple nested elements
tags = quote.find_all('a', class_='tag')
</code></code></pre><h2>Understanding Pagination</h2><p><strong>What is pagination?</strong></p><p>Websites split data across multiple pages:</p><ul><li><p>Page 1: quotes.toscrape.com/page/1/</p></li><li><p>Page 2: quotes.toscrape.com/page/2/</p></li><li><p>Page 3: quotes.toscrape.com/page/3/</p></li><li><p>...</p></li></ul><p><strong>How we handle it:</strong></p><pre><code><code>page_num = 1
has_more_pages = True

while has_more_pages:
    url = f'http://quotes.toscrape.com/page/{page_num}/'
    response = requests.get(url)
    
    # Check if page exists
    if response.status_code == 404:
        has_more_pages = False
        break
    
    # Scrape this page
    quotes = scrape_page(url)
    
    # Move to next page
    page_num += 1
</code></code></pre><p><strong>Detecting the last page:</strong></p><p>We know we&#8217;re on the last page when:</p><ul><li><p>HTTP status code is 404 (page not found)</p></li><li><p>Or there&#8217;s no &#8220;Next&#8221; button in the HTML</p></li><li><p>Or we find 0 quotes on the page</p></li></ul><h2>Understanding Function-Based Design</h2><p><strong>Today&#8217;s architecture uses only functions:</strong></p><pre><code><code># Each function does ONE thing

def fetch_page(url):
    """Fetch HTML from URL"""
    return html

def parse_quotes(html):
    """Extract quotes from HTML"""
    return quotes

def filter_by_tags(quotes, tags):
    """Filter quotes by tags"""
    return filtered_quotes

def save_to_json(quotes, filename):
    """Save quotes to JSON file"""
    # write to file
</code></code></pre><p><strong>Why functions first?</strong></p><p>&#9989; Simple to understand<br>&#9989; Easy to test each piece<br>&#9989; Clear data flow<br>&#9989; Good for small projects</p><p><strong>Limitations we&#8217;ll see tomorrow:</strong></p><p>&#10060; No shared state (passing data everywhere)<br>&#10060; Difficult to extend functionality<br>&#10060; Hard to manage related data<br>&#10060; No encapsulation</p><p><strong>Tomorrow we&#8217;ll refactor to classes</strong> and see how OOP solves these problems!</p><h2>Coming Tomorrow</h2><p>Tomorrow we&#8217;re <strong>refactoring this exact code to use classes</strong>! You&#8217;ll see how the same scraper becomes cleaner, more organized, and easier to extend when we introduce OOP concepts like encapsulation and methods!</p><h2>Skeleton and Solution</h2><p>Below you will find both a downloadable skeleton.py file to help you code the project with comment guides and the downloadable solution.py file containing the correct solution.</p><p>Get the code skeleton here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/view/w7twDp4p_DbcMJ1oRAAYiQ&quot;,&quot;text&quot;:&quot;View Code Skeleton&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/view/w7twDp4p_DbcMJ1oRAAYiQ"><span>View Code Skeleton</span></a></p><p></p><p>Get the code solution here:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://share.pythonanywhere.com/evolution/W-4QunaloRCXxtjSfLjWDw&quot;,&quot;text&quot;:&quot;View Code Solution&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://share.pythonanywhere.com/evolution/W-4QunaloRCXxtjSfLjWDw"><span>View Code Solution</span></a></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dailypythonprojects.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Daily Python Projects is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>