Feed: Ned Batchelder

Entries found: 10

Evergreen posts

Updated: 2026-10-28T08:41:28-04:00
UTC: 2026-10-28 12:41:28+00:00
URL: https://nedbatchelder.com/blog/202610/evergreen_posts

Because I’ve been writing this blog for so long, I often share links to very old posts. Three years ago, I added a feature: if the post is more than five years old, there’s a message next to the dateline indicating the age.
Content Preview

Because I’ve been writing this blog for so long, I often share links to very old posts. Three years ago, I added a feature: if the post is more than five years old, there’s a message next to the dateline indicating the age.

The age descriptions adjust automatically when the post is rendered. I had fun implementing the human-style age descriptions: “nearly 12 years”, “more than seven years”, “exactly nine years”, etc:

NUMBER_NAMES = dict(enumerate(

    "zero one two three four five six seven eight nine ten".split()
))

def years_age(age):
    years, frac = divmod(age.days / 365.25, 1)
    years = int(years)
    if frac < 1/365:
        modifier = "exactly "
    elif frac < 2/12:
        modifier = ""
    elif frac < 4/12:
        modifier = "more than "
    elif frac < 8/12:
        modifier = "over "
    elif frac < 9/12:
        modifier = "close to "
        years += 1
    elif frac < 10/12:
        modifier = "almost "
        years += 1
    elif frac < 11/12:
        modifier = "nearly "
        years += 1
    elif frac < 364/365:
        modifier = ""
        years += 1
    else:
        modifier = "exactly "
        years += 1
    years = NUMBER_NAMES.get(years, years)
    return f"{modifier}{years} years"

The complete message is then something like “This is more than 14 years old. Be careful” because many posts are technical, and details might have shifted. For example: Tricky locals() .

But some posts never become stale: they are evergreen. So back then I also added a way to mark blog posts as evergreen. The message would change to “This is eight years old, but it’s still good.” I marked blog posts evergreen one by one when I shared them and saw the needless “Be careful” warning.

Today I took it one step further: I made the “but it’s still good” message customizable on each post. For example:

One of the things I like about running this site is that it is all mine. I can change the software however I like. I can build features like this that truly don’t matter, but they make me smile. And that is always good.

Making git hooks visible

Updated: 2026-10-11T08:25:12-04:00
UTC: 2026-10-11 12:25:12+00:00
URL: https://nedbatchelder.com/blog/202610/making_git_hooks_visible

Frank Wiles wrote about an attack he nearly fell victim to: a downloaded git repo had active git hooks that would have compromised his system. Seeing the risks, I wanted to make git hooks more visible on my own machine.
Content Preview

Frank Wiles wrote about an attack he nearly fell victim to : a downloaded git repo had active git hooks that would have compromised his system. Seeing the risks, I wanted to make git hooks more visible on my own machine.

A git hook is a script in the .git directory of a git repo on your machine. They run during various git operations. The important detail to understand about hooks is that they do not get installed locally when you clone a repo. A newly cloned repo never has active hooks. Hooks have to be enabled after cloning, usually by you.

The attack Frank saw was dangerous because it wasn’t a cloned repo: it was a local checkout that had hooks installed then shared as a Dropbox folder. So when it landed on Frank’s machine, the hooks were active without him realizing it.

Git hooks are not apparent: they are meant to sit quietly until they run and ideally to be unobtrusive even then. But this attack made me want to know what hooks were in each of my git directories.

In zsh, you can define a function called chpwd , and it will execute whenever the current directory changes. Using this feature, I could create a script that lists the active hooks when I change into a git directory:

git_show_hooks() {

    local git_dir=$(git rev-parse --absolute-git-dir 2>/dev/null)
    if [[ -z $git_dir ]]; then
        _git_shown_hooks_dir=
        return
    fi
    local hooks_dir=$(git rev-parse --git-path hooks)
    hooks_dir=${hooks_dir:a}
    # Only announce once per repo, not for every cd inside it.
    if [[ $hooks_dir == $_git_shown_hooks_dir ]]; then
        return
    fi
    _git_shown_hooks_dir=$hooks_dir

    local -a hooks
    local hook
    setopt localoptions nullglob
    for hook in $hooks_dir/*; do
        if [[ -f $hook && -x $hook && $hook != *.sample ]]; then
            hooks+=(${hook:t})
        fi
    done
    if (( $#hooks )); then
        echo -e "\e[31mgit hooks:\e[0m $hooks (from $git_dir)"
    fi
}

autoload -Uz add-zsh-hook
add-zsh-hook chpwd git_show_hooks

Now entering a git repo I get an announcement of active hooks, if any. It’s very rare to see a hook other than pre-commit, but if any are there, they will be reported. The message will only be shown the first time I cd into a repo to keep the noise down.

Once I had this in place, there was another git hook consideration I could address: it’s common to have a .pre-commit-config.yaml file in a repo, but it’s easy to forget to enable the pre-commit hook that will actually use the file. Many developers probably have cloned repos where they haven’t enabled the pre-commit hook, so the desired checks aren’t running.

A second chpwd function can look for the configuration file and let you know if you have the pre-commit hook enabled:

git_check_precommit() {

    local toplevel=$(git rev-parse --show-toplevel 2>/dev/null)
    if [[ -z $toplevel ]]; then
        _git_shown_precommit_dir=
        return
    fi
    # Only announce once per repo, not for every cd inside it.
    if [[ $toplevel == $_git_shown_precommit_dir ]]; then
        return
    fi
    _git_shown_precommit_dir=$toplevel

    if [[ ! -f $toplevel/.pre-commit-config.yaml ]]; then
        return
    fi
    local hooks_dir=$(git rev-parse --git-path hooks)
    hooks_dir=${hooks_dir:a}
    local pc_tool=
    if [[ -f $hooks_dir/pre-commit ]]; then
        if grep -q 'File generated by prek:' $hooks_dir/pre-commit 2>/dev/null; then
            pc_tool=prek
        elif grep -q 'File generated by pre-commit:' $hooks_dir/pre-commit 2>/dev/null; then
            pc_tool=pre-commit
        fi
    fi
    if [[ -n $pc_tool ]]; then
        echo "$pc_tool installed"
    else
        echo -e "\e[31mpre-commit:\e[0m not installed," \
            "run 'pre-commit install' or 'prek install'" \
            "($toplevel/.pre-commit-config.yaml)"
    fi
}

add-zsh-hook chpwd git_check_precommit

One more step, just to be extra safe: zsh doesn’t run the chpwd function when starting a new shell. So we run the functions explicitly to check our initial directory:

# Run them once on shell startup to check our starting directory.

git_show_hooks
git_check_precommit

These shell functions are keeping me aware of the enabled git hooks. The messages might appear too often, and I might stop noticing them after a while, but I think this is much better than not knowing what’s going on. I haven’t seen any attacks, but I found at least one of my projects where I forgot to install pre-commit.

Expert asterisks

Updated: 2026-09-27T16:25:00-04:00
UTC: 2026-09-27 20:25:00+00:00
URL: https://nedbatchelder.com/blog/202609/expert_asterisks

Watching discussions happening online, I see a frequent unfortunate tic I’ll call the Expert Asterisk. A beginner is asking for help, and experts are answering. Then in the spirit of completeness, an expert throws in a fact or detail far from the learner’s abilities or needs.
Content Preview

Watching discussions happening online, I see a frequent unfortunate tic I’ll call the Expert Asterisk. A beginner is asking for help, and experts are answering. Then in the spirit of completeness, an expert throws in a fact or detail far from the learner’s abilities or needs.

As an example, here was an interaction about git: a new learner was having trouble getting properly oriented. They were asking how branches relate to directories, and how to undo a change. They knew about git init but didn’t know how it related to changing projects.

newb : so every time I change project I simply do ‘git init’ on them?

expert1 : ‘git init’ is for creating a new project.

expert2 : you change your working directory, typically, when you change project

So far so good, but then:

expert2 : or you specify the git repo directory with the -C switch, but it’s more typing to do, so hardly anyone does that unless there’s a very good reason

What!? Why mention this while explicitly pointing out no one does it? The newb just barely understands how git works at all. Why introduce obscure command-line options that “hardly anyone” uses?

I call this an asterisk because it feels like an esoteric footnote that only experts, academics, or completionists would need to know.

The expert is trying to be helpful. They are presenting all the options in the optimistic hope that the newb will be able to pick and choose among them.

Worse, sometimes multiple experts each offer their own asterisks, and the poor newb is intrigued and distracted by each one. The new learner doesn’t have a way to sift out which details might be important to them. Their original question gets lost in the spreading tangents.

It’s hard to help new learners: we can’t know where their gaps are. What misconceptions need to be unwound? Often we have only a glimpse of their context. Experts can miss when they use words slightly wrong, taking them at face value instead of seeing the misunderstanding that leads down the wrong path.

Notice in the exchange above, “change project” could mean switching among existing projects or starting a new project. We’re not sure which they meant. An expert will cycle among a number of active projects, but beginners will work on one until it is done, then switch to a new project, never returning to the previous one. Perhaps “every time I change project I do ‘git init’” was right for their situation.

The -C comment didn’t derail the session, but the effort of that comment could have instead been used to clarify the newb’s need.

I understand why experts add these asterisks to help sessions: they are useful for some people even if not for this beginner, and experts are interested (even fascinated) by all the intricate details. These discussion areas are not strictly for newb-help. They’re for general discussion among many people. There’s no rule against talking about advanced or rare options.

And it’s hard to know what will be useful, or what the newb really means, or what will resonate with them to help them find a solution. It’s all difficult.

But you should at least recognize who you are addressing and whether you are helping them. If you want to help the beginner, try to resist the impulse to add little-traveled tangents. Focus on the question at hand and try hard to understand the newb’s situation and mindset. Speak their language so they can hear you.

Silence is golden lightning talk

Updated: 2026-09-16T19:02:55-04:00
UTC: 2026-09-16 23:02:55+00:00
URL: https://nedbatchelder.com/blog/202609/silence_is_golden_lightning_talk

This is a lightning talk I did at PyCon US 2026: Silence is Golden. The overall message is to leave some quiet time so that reluctant speakers have a chance to participate. I start with a jokey disclaimer because I followed Simon Willison who gave an energetic, entertaining, and loud(!) speed-run through a year of progress in LLMs:
Content Preview

This is a lightning talk I did at PyCon US 2026: Silence is Golden. The overall message is to leave some quiet time so that reluctant speakers have a chance to participate. I start with a jokey disclaimer because I followed Simon Willison who gave an energetic, entertaining, and loud(!) speed-run through a year of progress in LLMs:

I do these talks and blog posts about how to better interact with people, but I hope they don’t come off as too preachy. I have to remind myself to keep these ideas top of mind. I am one of those people who speak easily that I describe in the lightning talk. I have to remember to hold back, to leave time and space for other people.

I had the idea for this lightning talk a few years ago at PyCon, while watching things going wrong. I was in a room with a few dozen people. The goal in the room was to hear from lots of people, but the leader of the discussion was a “speaks easily” kind of person, and I could see the dynamic of the room failing to make space for everyone.

I was really pleased to be able to extend the time-tested and well-known Pac-Man Rule from space to time. It made for an interesting hook, making the talk more interesting than just a scold.

It’s really hard to keep quiet. But it’s important sometimes.

Micro language implementation: Calcium

Updated: 2026-08-22T15:53:36-04:00
UTC: 2026-08-22 19:53:36+00:00
URL: https://nedbatchelder.com/blog/202608/micro_language_implementation_calcium

I wrote a tiny language implementation: Calcium. It’s meant as a demonstration of how languages like Python are implemented. It has a tokenizer, a parser, an AST, a compiler, bytecodes, and an execution engine, all in about 300 lines of code.I did it because I often see the question: isn’t Python interpreted? Why do people say it’s compiled? (BTW, I also answered this in an earlier blog post: Is Python interpreted or compiled? Yes.) It can be hard to explain that your Python program never becomes an explicit sequence of native CPU instructions, which is what people often think “compiled” means.So I coded up Calcium to have on hand the next time it comes up. I think it will help to be able to show the execution engine code reading bytecodes and doing what they say.It could also be an interesting starting point for people wanting to play with a language implementation. It has almost nothing, so there’s lots of simple things (comments?) to add.
Content Preview

I wrote a tiny language implementation: Calcium . It’s meant as a demonstration of how languages like Python are implemented. It has a tokenizer, a parser, an AST, a compiler, bytecodes, and an execution engine, all in about 300 lines of code.

I did it because I often see the question: isn’t Python interpreted? Why do people say it’s compiled? (BTW, I also answered this in an earlier blog post: Is Python interpreted or compiled? Yes. ) It can be hard to explain that your Python program never becomes an explicit sequence of native CPU instructions, which is what people often think “compiled” means.

So I coded up Calcium to have on hand the next time it comes up. I think it will help to be able to show the execution engine code reading bytecodes and doing what they say.

It could also be an interesting starting point for people wanting to play with a language implementation. It has almost nothing, so there’s lots of simple things (comments?) to add.

Caller-specific coverage

Updated: 2026-08-09T13:56:25-04:00
UTC: 2026-08-09 17:56:25+00:00
URL: https://nedbatchelder.com/blog/202608/callerspecific_coverage

I’ve had an idea rattling around to get more detail from coverage measurement. Can we measure the coverage in a function separately for each caller of the function?
Content Preview

I’ve had an idea rattling around to get more detail from coverage measurement. Can we measure the coverage in a function separately for each caller of the function?

Here’s why I want it: in Acidica , my toy BASIC interpreter, I had code to implement the built-in functions that looked something like this:

match func_name:


    case "LEN":
        if len(args) != 1:
            raise TypeError(f"Wrong arguments for LEN, got {len(args)}")
        return len(args[0])

    case "LEFT$":
        if len(args) != 2:
            raise TypeError(f"Wrong arguments for LEFT$, got {len(args)}")
        return args[0][:args[1]]

    # ... 19 other built-ins ...

I didn’t like the repeated code here: each different func_name has to check that it got its expected number of arguments and perhaps raise an error. So I refactored:

def expects(nargs: int, func_name: str, args: tuple) -> None:

    if len(args) != nargs:
        raise TypeError(f"Wrong arguments for {func_name}, got {len(args)}")

match func_name:
    case "LEN":
        expects(1, func_name, args)
        return len(args[0])

    case "LEFT$":
        expects(2, func_name, args)
        return args[0][:args[1]]

Nice. The code is tighter, easier to read, and common behavior is implemented in one place.

But the old code had an advantage: because each error condition had its own raise line, coverage measurement could tell me whether I had tested every func_name for the wrong number of arguments. With the error handling happening in a helper function, that information is lost. I’ll know that some func_name had a test for the wrong number of arguments, but not that all of them did.

Here’s where the new idea comes in. What if I could indicate that for the expects function, I want separate coverage data for each distinct calling site? Then I could see that every func_name had a test for both the wrong number of arguments and the right number of arguments. The simple branch inside expects would be measured separately for each caller.

I have a quick proof-of-concept. A decorator on expects does the work. Coverage.py already has dynamic contexts which are used for things like tracking which tests called which code. The decorator starts a new context named for the calling location, then restores the context when the function returns:

def coverage_per_caller(func):

    @functools.wraps(func)
    def _wrapper(*args, **kwargs):
        cov = coverage.Coverage.current()
        name = func.__name__
        caller = inspect.currentframe().f_back
        file = caller.f_code.co_filename
        lineno = caller.f_lineno
        prev_context = cov.switch_context(f"per_caller:{name}:{file}:{lineno}")
        try:
            ret = func(*args, **kwargs)
        finally:
            cov.switch_context(prev_context)
        return ret

    return _wrapper

I had to make one tiny (unreleased) change to coverage.py for this: switch_context used to return None, but now it returns the previous context so that we can nest them properly.

To my delight, this works! I can look at the HTML coverage report and see the caller contexts for the lines in expects . I can see that 20 callers ran the if line, but only 2 ran the raise , and the context names show the file and line number of the callers for each:

This isn’t the whole solution yet. Things to improve:

  • I’d like to post-process these contexts to show which callers were missing lines inside expects . What I’m looking for is the same kind of “this line is missing” information that I got from the original inlined logic.
  • These per-caller contexts overwrite the contexts we were already collecting (the test names). Ideally we’d have some kind of sub-context so that we could track both (or many) at once.
  • It’s not great that I had to add a decorator to the source code. Driving this through the coverage configuration would keep these kinds of details out of the source.

But it’s a start, and gives me other ideas. I could use some aspect of the data passed into a function as the context name. In this example, we could have used func_name as the context instead of the caller’s location. Maybe you have ideas for other uses.

Acidica

Updated: 2026-07-26T10:54:28-04:00
UTC: 2026-07-26 14:54:28+00:00
URL: https://nedbatchelder.com/blog/202607/acidica

My latest fun project is a BASIC interpreter called Acidica. Classic BASIC is an old-school language first developed in 1964 that saw an explosion of implementations on microcomputers in the ‘70s and ‘80s. It’s much more primitive than the Visual Basic that you might be familiar with.
Content Preview

My latest fun project is a BASIC interpreter called Acidica . Classic BASIC is an old-school language first developed in 1964 that saw an explosion of implementations on microcomputers in the ‘70s and ‘80s. It’s much more primitive than the Visual Basic that you might be familiar with.

A simple BASIC program:

10 INPUT "What is your name"; U$

20 PRINT "Hello "; U$
30 INPUT "How many stars do you want"; N
40 S$ = ""
50 FOR I = 1 TO N
60 S$ = S$ + "*"
70 NEXT I
80 PRINT S$
90 INPUT "Do you want more stars"; A$
100 IF LEN(A$) = 0 THEN 90
110 A$ = LEFT$(A$, 1)
120 IF A$ = "Y" OR A$ = "y" THEN 30
130 PRINT "Goodbye ";U$
140 END

Run it, and you get this:

What is your name? Ned

Hello Ned
How many stars do you want? 10
**********
Do you want more stars? y
How many stars do you want? 20
********************
Do you want more stars? n
Goodbye Ned

The wide variety of BASIC flavors meant I first had to decide what to implement. I found Vintage BASIC and used its spec, both because it is concisely described, and because it has an implementation I could run to double-check behavior when I had questions. The site also has a collection of runnable games from Creative Computing magazine , which I remember fondly.

This was a perfect vacation-week project. It has no real-world consequences. It had some interesting problems to puzzle through. It was testable. It satisfied some nostalgia for my earlier computing days. It was bounded enough to be “done”.

In those ways, it’s very similar to a vacation project of mine from four years ago: Stilted , an implementation of PostScript.

Acidica is not useful for writing new programs, only because BASIC itself is so difficult. There is no scoping beyond single-line functions, variables names can be as long as you want but only the first two letters and first digit are significant. Keywords are recognized anywhere, so FACTOR can’t be variable name because it has TO in the middle. The only control structures are FOR, IF, and GOTO. It’s something of a testament to human persistence that programs like three-dimensional tic-tac-toe can be written in it.

As a side project, I could choose my development style: no real type checking (partly because BASIC’s values would be awkward to squeeze into static typing), and very few docstrings. There are lots of tests, but only integration tests: every test is a BASIC program to run, with a check for the correct output and/or the expected error.

To be honest, the “only integration tests” approach was kind of a pain, but I stuck with it and resisted the temptation to add unit tests along the way.

Another choice I made: no AI. I like writing programs. I get a deeper sense of the thing I am building when I have my fingers in the clay. Since there was no deadline, or even any reason to ever finish the project, I could take my time and not be rushed.

But I like the result. I enjoyed the time I spent working on it. I liked being able to stop and devote pure thinking time while doing other things when I got to the next hurdle. The next steps here might be to use this project as a test bed for some development ideas. Or maybe add a BASIC-to-Python transpiler. Or maybe it’s done.

A place of certainty

Updated: 2026-07-14T06:21:24-04:00
UTC: 2026-07-14 10:21:24+00:00
URL: https://nedbatchelder.com/blog/202607/a_place_of_certainty

My mother is 86, and she is declining. Things that used to be easy for her now seem completely foreign. She was a programmer, writing software before I could read, so it is very strange to see her like this.
Content Preview

My mother is 86, and she is declining. Things that used to be easy for her now seem completely foreign. She was a programmer, writing software before I could read, so it is very strange to see her like this.

She no longer uses a computer. If I mention some photos I found online, she asks if there’s any way she can see them, as if she has never used the internet. This is a new reality for me, but is easier than a year or two ago when she still tried to be constantly online. As things got more confusing for her, she struggled and complained “the computer is haunted.” Now she doesn’t have the computer as a source of friction, but also not as a center of activity.

In many ways, she is following a similar path to her own mother, my Grandma O . Like her, my mom is accepting the changes in her relationship to the world. She is able to laugh at it a bit. But it will still be difficult, especially because we know it is a progression that is not going to get better and will very likely get worse.

The new her is very different from the original her. She was not timid. She came out as gay in the mid ‘70s and ran a feminist bookstore . She worked as a programmer. She got a PhD in computational linguistics just because she was interested in the topic. These were the things I was used to hearing about from her. She never lacked for enthusiasms, projects and accomplishments.

She was always energetic and feisty, ready to engage in debate. This picture does a good job capturing the spirit of many of our interactions in the past:

My mom and me in a lively but good-spirited debate

Now she is mild and somewhat resigned. She says things like, “I don’t think much anymore.” I know there are other ways this could go. Some people get very angry as their abilities fade. In that sense, this is a good trajectory, but I am still sad to see her shrink.

Last week we had a family gathering at my sister’s house, the usual location for these big events. My mom has been there many times. But now she didn’t recognize it. I sat with my mom and sister over lunch. They were discussing the dining room we were in. It wasn’t familiar to my mom. She wasn’t upset about it, just looked around and said, “no, I don’t remember this.”

My mom was enjoying her salad, but eating it with her hands. I pointed to the fork on her plate and asked, “You don’t like the fork?” She looked at it as if it was some unimportant detail of the tablecloth, and kept eating with her hands. She wasn’t bothered, just calmly proceeded in her way.

At the end of the party, my mom and her wife Fumiko were getting ready to go. Fumiko had scheduled a ride-share car, so we went out to the street to wait for it. We brought out a chair for my mom to sit. The time for the car came and went, but no car arrived. There were five of us out there: me, my sister and brother, my mother and Fumiko. My brother and Fumiko were trying to figure out where the car was. They were looking through the app for information. They re-read the email confirming the scheduled ride. Should we keep waiting? We could request a new ride. Would we be charged for the missed scheduled ride? It was a whole thing, lots of discussion and questions.

In the middle of this, without warning, my mom tried unsteadily to get up from her chair. Two of us quickly intercepted her. The uneven pavement seemed particularly treacherous for her. We supported her arms to keep her steady.

“Mom, where are you trying to go?”

“I want a place of certainty. This place seems very uncertain.”

She was right: out there on the sidewalk we were all uncertain. But I have to wonder if she was also talking about her larger experience in a world that is less and less understandable for her.

My mom sitting on her chair on the sidewalk with her three children standing behind her, waiting for the car

In the back of my mind, I wonder what my own future holds. But that is decades away, and my mother’s situation is now. I don’t know what her next steps down will be like. She has already changed a great deal in the last year.

I think we would all like a place of certainty. I know I would, but I also know I am not going to get it soon.

Dodecahedron with stars

Updated: 2026-06-18T06:15:20-04:00
UTC: 2026-06-18 10:15:20+00:00
URL: https://nedbatchelder.com/blog/202606/dodecahedron_with_stars

I saw this dodecahedron with an Islamic-inspired pattern designed by Taj Ragoo. As soon as I saw it, I knew I had to make one. I studied the pattern, wrote some Python, and made myself a PDF. I cut it out, folded it, glued it together, and now I have one of my own:
Content Preview

I saw this dodecahedron with an Islamic-inspired pattern designed by Taj Ragoo . As soon as I saw it, I knew I had to make one. I studied the pattern, wrote some Python, and made myself a PDF. I cut it out, folded it, glued it together, and now I have one of my own:

A paper dodecahedron on a cluttered desk

I love that this elegantly combines two pure geometric forms: the Platonic dodecahedron (12 uniform pentagons), and an Islamic pattern using five-pointed stars.

Looking closely, details emerge:

The dodecahedron with some regions of the pattern highlighted in colors

Each face has ten small stars in a ring. I’ve lightened them a bit in the front face here. At the center of each face is a ten-pointed star (highlighted in red), made of two overlaid five-pointed stars.

The real genius of the pattern is at the corners. I’ve highlighted one in blue. It’s a star made of the same parts as the central ten-pointed star, but there are only nine points. It works because three pentagons lying flat touching at a point occupy 324 degrees, leaving a 36-degree gap.

When the dodecahedron is folded together, the gap is closed. 36 degrees is exactly one-tenth of a complete 360-degree circle, so exactly one point of the ten-pointed star is missing, leaving a perfect nine-pointed star using the same shapes, spread over the corners of three pentagons. Beautiful!

If this appeals to you, follow Taj on Instagram : he’s got more Platonic/Islamic mashups to enjoy. The paper versions are just prototypes of the final versions he makes in wood.

Of course, you can get my PDF and make one for yourself:

The Python code to draw the net isn’t great: it has no real parallels to the structure of each face. It’s a lot of math and line drawing to get things in the right places. My ideal would be to have a toolset that used a tile-placing abstraction, to be able to do more interesting designs. Some day.

It was a joy to work on this though. It was a slow process of studying the original, working out the math, then mulling over coding approaches. The code was developed in small steps over weeks. Then printing initial versions, marking them up, working out the tab structure. Some copies were colored to understand how the lines flowed across the whole dodecahedron. It was good to be working in both the mental and physical worlds:

Various stages of progress in a messy pile

Update: it looks like the design was originally by Dana Awartani: Dodecahedron Within an Icosahedron .

Cultural factoids of the day: 64

Updated: 2026-06-16T06:16:06-04:00
UTC: 2026-06-16 10:16:06+00:00
URL: https://nedbatchelder.com/blog/202606/cultural_factoids_of_the_day_64

Perhaps because 64 is a power of two, and a square and a cube, but also for other reasons, it pops up in lots of places. Here are some of the things I’ve associated it with over the years:
Content Preview

Perhaps because 64 is a power of two, and a square and a cube, but also for other reasons, it pops up in lots of places. Here are some of the things I’ve associated it with over the years:

Crayola 64-crayon box : as a kid, this box seemed like the ultimate luxury, the Rolls-Royce of crayons. So many colors, and the box had a built-in sharpener. Advanced technology!

¶ A chess board has 64 squares, and is used as the setting for the age-old question about doubling: would you rather have one billion dollars, or a penny on the first square, then double the number on each next square? It’s an eye-opening demonstration of exponential growth and how big numbers can get. Take the chess board: you’ll have 184 million times more money!

¶ I grew up in New York City too late to visit the 1964 World’s Fair , but its aura hung over the city. I was always fascinated by it, and still am. It epitomized the early 60’s optimism about the future. This Love of Theme Parks video does a good job capturing the Fair’s original spirit and the current state of the location, and explains the important part Walt Disney played in the whole thing.

¶ As a power of 2, it appears in many tech things: Nintendo 64, Commodore 64, base-64 encoding, 64-bit integers, 64-bit computing in general, and so on and so on.

¶ The number famously appears in the Beatles’ song When I’m Sixty-Four . A surprising fact about the song is that it’s one of the first Paul McCartney ever wrote, when he was about 14 years old. It’s an old-fashioned tune because he wasn’t aware of rock and roll yet, or maybe it hadn’t even happened yet. It’s thought they put the song on Sgt Pepper because Paul’s father was turning 64 that year.