Ephes Blog

Miscellaneous things. Mostly Weeknotes and links I stumbled upon.


Weeknotes 2025-08-11

Taking a summer break this week. Released a new version of django-cast with Wagtail 7.1 support, and just sharing a handful of links.

Articles


Weeknotes 2025-08-04


Weeknotes 2025-07-28 - Sentinel Values, IndieWeb Updates, and New Hardware

Are you fine (fucked up, insecure, needy, and emotional)? --amy tech (bones)

Building a Better Blogging Habit

I've been thinking about blogging more regularly, even if it's just quick "Today I Learned" posts whenever I discover a new approach to something. In our latest podcast episode, we discussed sentinel values in Python, and my Auto sentinel approach didn't go over well. We even received an email from a listener suggesting improvements, which got me thinking—this deserves a blog post. The takeaway? Avoid sentinel values unless you can't use None because it already has special meaning (like representing null in a database). I also wrote about handling editable installs with uv, since running uv pip install -e ../some-dependency followed by uv run pytest can cause issues when uv run internally calls uv sync. While I don't have a perfect solution yet, my current workaround might help others facing the same problem.

IndieWeb Integration and Community

This blog now supports webmentions thanks to the latest django-indieweb release. While implementing this feature, I also added proper microformat annotations, "continue reading" links, a profile photo, and a now page. Speaking of IndieWeb, I attended a local Homebrew Website Club meeting, which was fantastic. I discovered that my Bridgy connection between this blog and Mastodon was already functioning.

New Hardware and Language Switching Adventures

Concerned about Claude Code potentially becoming unavailable or prohibitively expensive, I decided to invest in hardware for running models locally. While open-weight models aren't quite at the level of commercial offerings yet, they're rapidly improving. I'm excited to start testing on my new 128GB M4 Mac Studio (the M3 Ultra looked tempting, but I couldn't justify the price). This might inspire me to update my awesome-devenv repository and share my software setup and new Mac configuration process. I'm considering Ansible for automation, though I'm not sure the effort is worthwhile for something I do so infrequently.

I also picked up a new keyboard that was only available in US layout, and now I'm battling decades of muscle memory just to type properly. Here's an amusing side effect: after switching to the US keyboard layout and setting English as the primary language on my Mac Studio, Siri on my Apple Watch suddenly forgot how to understand German. This definitely shouldn't happen—maybe it's Siri's way of protesting the change!

Articles

Software

Videos


TIL: Managing Editable Dependencies in uv Without Committing Local Paths

The Problem

When developing Python packages that depend on each other, you often need to install dependencies in editable mode to see changes immediately. With `uv`, I was running into an annoying workflow issue:

  1. Install a dependency as editable: `uv pip install -e ../django-cast`
  2. Run tests or dev server: `uv run pytest` or `just dev`
  3. uv automatically runs `uv sync`, which reinstalls the package from git, overwriting my editable install!

This meant I had to reinstall editable packages after every `uv run` command, which was frustrating and slowed down development. There's also an existing GitHub issue for that.

Continue reading →


Mutable Defaults in Python: When None Isn't Enough

This post walks through Python's mutable default gotcha and explores modern solutions for when the standard None workaround isn't sufficient.

The Mutable Default Gotcha

Every Python developer eventually learns about the mutable default argument gotcha. Here's what happens:

# BAD - shared mutable default
def add_item(item, items=[]):
    items.append(item)
    return items

# First call works as expected
print(add_item("a"))  # ['a']

# But the second call reveals the problem!
print(add_item("b"))  # ['a', 'b'] - wait, what?

The issue is that default arguments are evaluated once when the function is defined, not each time it's called. So all calls to add_item() without providing items share the same list object!

The Standard Solution

This leads to the standard solution using None:

def add_item(item: str, items: list[str] | None = None) -> list[str]:
    if items is None:
        items = []
    items.append(item)
    return items

This works perfectly well for most cases. The list[str] | None annotation accurately represents what callers can pass, even though it looks a bit ugly and we immediately replace None with an empty list internally.

When None Has Meaning

The real trouble starts when None is a legitimate value in your domain:

  • Database ORMs where None represents NULL
  • Configuration systems where None means "inherit from parent"
  • Data processing where None means "no value provided" (different from "empty collection")

Now you need to distinguish between "no argument provided" and "explicitly passed None". This is where sentinel values come in.

The Search for a Better Pattern

When I read about Luke Plant's Auto sentinel pattern, it seemed elegant. The idea was to use a falsy sentinel:

Continue reading →