<?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"><channel><title><![CDATA[Tony Sterlin Cantave]]></title><description><![CDATA[Tony Sterlin Cantave]]></description><link>https://tonycantave.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 11:04:31 GMT</lastBuildDate><atom:link href="https://tonycantave.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Use Git on Small Solo Projects]]></title><description><![CDATA[Most of my code lives in small projects with exactly one contributor: me. A screen scheduling script, a folder of Python helpers, a data cleanup tool. For…]]></description><link>https://tonycantave.hashnode.dev/how-i-use-git-on-small-solo-projects</link><guid isPermaLink="true">https://tonycantave.hashnode.dev/how-i-use-git-on-small-solo-projects</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[Tony Cantave]]></dc:creator><pubDate>Fri, 06 Nov 2026 13:53:24 GMT</pubDate><content:encoded><![CDATA[<p>Most of my code lives in small projects with exactly one contributor: me. A screen scheduling script, a folder of Python helpers, a data cleanup tool. For a while I treated Git on those projects as a backup button. I typed a commit once a day with a message like "updates" and moved on. Then I needed to find the exact change that broke something, and a month of "updates" told me nothing.</p>
<p>Git is not only for teams. On a solo project it is your memory, your undo button and your safety net. Here is the simple routine I use now, with the commands that matter and the habits that make them useful.</p>
<h2>Start every project the same way</h2>
<p>The first three minutes of a project decide how clean its history stays. I run this in a new folder:</p>
<pre><code class="language-bash">git init
git status
</code></pre>
<p>Then, before the first commit, I create a .gitignore file. This keeps secrets, virtual environments and generated files out of the history, where they are painful to remove later.</p>
<pre><code class="language-text">.env
venv/
__pycache__/
*.log
output/
</code></pre>
<p>The .env line matters most. API keys and passwords belong in that file, never in code, and the ignore file makes sure Git never picks them up. If a secret does land in a commit, treat it as exposed and rotate it. Deleting the file afterward does not remove it from the history.</p>
<h2>Commit small, and write messages for your future self</h2>
<p>The habit that changed the most for me was committing one idea at a time. Not one day at a time. If I fix how a script reads dates and also rename a few variables, those are two commits.</p>
<pre><code class="language-bash">git status
git diff
git add reader.py
git commit
</code></pre>
<p>I always run git status and git diff before committing. Status shows which files changed. Diff shows exactly what changed inside them. More than once that quick look has caught a debug print or a test value I forgot to remove.</p>
<p>Running git commit without a message flag opens your editor, which nudges you to write a real message. My rule is simple: the first line finishes the sentence "This commit will...". So instead of "updates" I write "Skip blank rows when reading order files" or "Retry API calls after a timeout". Six months later, those lines read like a changelog.</p>
<h2>Use a branch for anything you might throw away</h2>
<p>Branches feel like overkill when you work alone, but they are the cheapest experiment you can run. When I want to try a different approach, I make a branch first.</p>
<pre><code class="language-bash">git branch retrylogic
git switch retrylogic
</code></pre>
<p>Now I can rewrite as much as I want. If the idea works, I switch back and merge it:</p>
<pre><code class="language-bash">git switch main
git merge retrylogic
</code></pre>
<p>If it does not work, I switch back to main and delete the branch with git branch and the delete option. Either way, main stays in a state I trust. This is the same thinking I used for years in manufacturing: you do not test a new process on the line that ships product. You test it on the side, then move it over once it proves itself.</p>
<h2>Know your undo commands</h2>
<p>Most Git anxiety comes from not knowing how to back out. These three cover nearly everything I need on a solo project:</p>
<pre><code class="language-bash">git restore reader.py
git stash
git stash pop
</code></pre>
<p>The first throws away uncommitted changes in a file and returns it to the last commit. Stash sets aside everything you are working on so you can switch tasks, and stash pop brings it back. For anything already committed, git revert creates a new commit that undoes an old one, which is safer than rewriting history.</p>
<h2>Find the commit that broke things</h2>
<p>This is where good commit messages pay off. When a script that worked last week fails today, I look at the history first:</p>
<pre><code class="language-bash">git log
git show
</code></pre>
<p>Log lists commits with their messages. Show displays the full change for any commit you name by its ID. With small, clearly described commits, I can usually spot the guilty change by reading the list. For harder cases, git bisect walks through the history with you, checking out commits halfway between a good version and a bad one until it narrows down the exact commit that introduced the problem. On a project with a few hundred commits, that takes only a handful of steps.</p>
<h2>Push somewhere off your machine</h2>
<p>A repository that only exists on one laptop is one spilled coffee away from gone. I push every project to GitHub, private if it is not ready to share:</p>
<pre><code class="language-bash">git remote add origin https://github.com/yourname/yourproject.git
git push origin main
</code></pre>
<p>After that first setup, pushing at the end of each work session is a single command, and the project is backed up with its full history.</p>
<h2>The routine in one paragraph</h2>
<p>Ignore secrets before the first commit. Check status and diff before every commit. Commit one idea at a time with a message that finishes "This commit will". Use a branch for experiments. Learn restore, stash and revert so undo never feels scary. Push at the end of every session. None of this is advanced Git. It is just enough structure that when something breaks, the history tells you what happened instead of leaving you to guess.</p>
<p><em>Originally published at <a href="https://tonycantave.com/blog/using-git-on-small-solo-projects">tonycantave.com</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[How I Write Small Tests for Python Scripts]]></title><description><![CDATA[Most of the Python I write is not a big application. It is scripts: a file that cleans a CSV, a function that formats data for a report, a helper that…]]></description><link>https://tonycantave.hashnode.dev/how-i-write-small-tests-for-python-scripts</link><guid isPermaLink="true">https://tonycantave.hashnode.dev/how-i-write-small-tests-for-python-scripts</guid><category><![CDATA[Python]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Tony Cantave]]></dc:creator><pubDate>Thu, 05 Nov 2026 13:55:24 GMT</pubDate><content:encoded><![CDATA[<p>Most of the Python I write is not a big application. It is scripts: a file that cleans a CSV, a function that formats data for a report, a helper that checks API responses. For a long time I tested those scripts the way most people do. I ran them, looked at the output, and decided it seemed fine. That works until the day you change one line and something three steps away quietly breaks.</p>
<p>Writing and debugging Python samples as part of my AI evaluation work changed how I think about this. When you review code all day, you learn that the bugs that hurt most are not the dramatic crashes. They are the small wrong answers nobody notices. A few tests catch those early, and they do not take long to write.</p>
<h2>Start by pulling logic into functions</h2>
<p>You cannot easily test a script that does everything at the top level. The first step is moving the real work into small functions that take input and return output. Reading files, printing and calling APIs stay at the edges. The decisions live in the middle, where a test can reach them.</p>
<p>Here is a small example. Say a script reads order amounts as text and needs to turn them into numbers, skipping anything invalid. This lives in a file called amounts.py.</p>
<pre><code class="language-python">def parse_amount(text):
    cleaned = text.strip().replace("$", "").replace(",", "")
    if cleaned == "":
        return None
    try:
        value = float(cleaned)
    except ValueError:
        return None
    if value &lt; 0:
        return None
    return round(value, 2)


def sum_file(path):
    total = 0.0
    for line in path.read_text().splitlines():
        value = parse_amount(line)
        if value is not None:
            total += value
    return round(total, 2)
</code></pre>
<h2>Write the first tests</h2>
<p>Install pytest with pip install pytest, then create a file whose name starts with test_, such as test_amounts.py. Every function in it whose name starts with test_ is a test.</p>
<pre><code class="language-python">from amounts import parse_amount

def test_plain_number():
    assert parse_amount("42") == 42.0

def test_dollar_sign_and_commas():
    assert parse_amount("$1,250.50") == 1250.5

def test_blank_is_none():
    assert parse_amount("   ") is None

def test_text_is_none():
    assert parse_amount("ten dollars") is None
</code></pre>
<p>Run pytest in that folder and it finds the file, runs every test, and tells you which ones failed and why. Plain assert statements are enough. When one fails, pytest shows the actual value next to the expected one, which is usually all you need to find the problem.</p>
<h2>Test the edges, not just the happy path</h2>
<p>The test that proves "42" becomes 42.0 is the least valuable one. The useful tests sit at the edges: blank strings, extra spaces, symbols, values that are far too large, and input in a format you did not expect. When I review code, the edge cases are where most bugs hide, and the same is true for my own scripts.</p>
<p>A good habit is to ask one question before writing tests: what is the strangest input this function will realistically see? Then write a test for exactly that.</p>
<h2>Use parametrize to keep tests short</h2>
<p>When you have many similar cases, pytest lets you list them in one place.</p>
<pre><code class="language-python">import pytest
from amounts import parse_amount

@pytest.mark.parametrize("text, expected", [
    ("42", 42.0),
    ("$1,250.50", 1250.5),
    (" 7.1 ", 7.1),
    ("", None),
    ("abc", None),
])
def test_parse_amount(text, expected):
    assert parse_amount(text) == expected
</code></pre>
<p>Each row runs as its own test. Adding a new case becomes a one line change, which means you will actually do it.</p>
<h2>Turn every bug into a test</h2>
<p>This is the habit that pays off most. When something breaks, I write a test that reproduces the bug before I fix it. I run it, watch it fail, fix the code, and watch it pass. Now that bug cannot come back without the test catching it. Over time the test file becomes a record of every mistake the script has already survived.</p>
<h2>Keep tests fast and independent</h2>
<p>Tests that touch real files, networks or databases get slow and unreliable. For small scripts I avoid that by passing data into functions instead of having them load it themselves. When a function really does need a file, pytest provides a built in tmp_path fixture that gives each test its own empty folder, so tests never step on each other or on your real data.</p>
<pre><code class="language-python">from amounts import sum_file

def test_sum_file_skips_bad_lines(tmp_path):
    sample = tmp_path / "orders.txt"
    sample.write_text("10\n$20.50\nnot a number\n\n")
    assert sum_file(sample) == 30.5
</code></pre>
<p>That one test checks the file reading, the parsing and the skipping of bad lines together, without touching anything outside a temporary folder.</p>
<h2>What this changed for me</h2>
<p>More than fifteen years in logistics and manufacturing taught me that checks built into a process beat inspection at the end. Tests are the same idea applied to code. They are not about perfection. They are about being able to change a script on Monday without worrying about what you broke in the version from Friday. Start with three tests for the function you trust least, run them every time you make a change, and add one more each time something surprises you.</p>
<p><em>Originally published at <a href="https://tonycantave.com/blog/writing-small-tests-for-python-scripts">tonycantave.com</a>.</em></p>
]]></content:encoded></item></channel></rss>