Horseless intelligence

Monday 17 March 2025

AI is everywhere these days, and everyone has opinions and thoughts. These are some of mine.

Full disclosure: for a time I worked for Anthropic, the makers of Claude.ai. I no longer do, and nothing in this post (or elsewhere on this site) is their opinion or is proprietary to them.

How to use AI

My advice about using AI is simple: use AI as an assistant, not an expert, and use it judiciously. Some people will object, “but AI can be wrong!” Yes, and so can the internet in general, but no one now recommends avoiding online resources because they can be wrong. They recommend taking it all with a grain of salt and being careful. That’s what you should do with AI help as well.

We are all learning how to use AI well. Prompt engineering is a new discipline. It surprises me that large language models (LLMs) give better answers if you include phrases like “think step-by-step” or “check your answer before you reply” in your prompt, but they do improve the result. LLMs are not search engines, but like search engines, you have to approach them as unique tools that will do better if you know how to ask the right questions.

If you approach AI thinking that it will hallucinate and be wrong, and then discard it as soon as it does, you are falling victim to confirmation bias. Yes, AI will be wrong sometimes. That doesn’t mean it is useless. It means you have to use it carefully.

I’ve used AI to help me write code when I didn’t know how to get started because it needed more research than I could afford at the moment. The AI didn’t produce finished code, but it got me going in the right direction, and iterating with it got me to working code.

One thing it seemed to do well was to write more tests given a few examples to start from. Your workflow probably has steps where AI can help you. It’s not a magic bullet, it’s a tool that you have to learn how to use.

The future of coding

In beginner-coding spaces like Python Discord, anxious learners ask if there is any point in learning to code, since won’t AI take all the jobs soon anyway?

Simon Willison seems to be our best guide to the head-spinning pace of AI development these days (if you can keep up with the head-spinning pace of his blog!) I like what he said recently about how AI will affect new programmers:

There has never been a better time to learn to code — the learning curve is being shaved down by these new LLM-based tools, and the amount of value people with programming literacy can produce is going up by an order of magnitude.

People who know both coding and LLMs will be a whole lot more attractive to hire to build software than people who just know LLMs for many years to come.

Simon has also emphasized in his writing what I have found: AI lets me write code that I wouldn’t have undertaken without its help. It doesn’t produce the finished code, but it’s a helpful pair-programming assistant.

Can LLMs think?

Another objection I see often: “but LLMs can’t think, they just predict the next word!” I’m not sure we have a consensus understanding of what “think” means in this context. Airplanes don’t fly in the same way that birds do. Automobiles don’t run in the same way that horses do. The important thing is that they accomplish many of the same tasks.

OK, so AI doesn’t think the same way that people do. I’m fine with that. What’s important to me is that it can do some work for me, work that could also be done by people thinking. Cars (“horseless carriages”) do work that used to be done by horses running. No one now complains that cars work differently than horses.

If “just predict the next word” is an accurate description of what LLMs are doing, it’s a demonstration of how surprisingly powerful predicting the next word can be.

Harms

I am concerned about the harms that AI can cause. Some people and organizations are focused on Asimov-style harms (will society collapse, will millions die?) and I am glad they are. But I’m more concerned with Dickens-style harms: people losing jobs not because AI can do their work, but because people in charge will think AI can do other people’s work. Harms due to people misunderstanding what AI does and doesn’t do well and misusing it.

I don’t see easy solutions to these problems. To go back to the car analogy: we’ve been a car society for about 120 years. For most of that time we’ve been leaning more and more towards cars. We are still trying to find the right balance, the right way to reduce the harm they cause while keeping the benefits they give us.

AI will be similar. The technology is not going to go away. We will not turn our back on it and put it back into the bottle. We’ll continue to work on improving how it works and how we work with it. There will be good and bad. The balance will depend on how well we collectively use it and educate each other, and how well we pay attention to what is happening.

Future

The pro-AI hype in the industry now is at a fever pitch, it’s completely overblown. But the anti-AI crowd also seems to be railing against it without a clear understanding of the current capabilities or the useful approaches.

I’m going to be using AI more, and learning where it works well and where it doesn’t.

Faster branch coverage measurement

Sunday 9 March 2025

After nearly two years, I think this is finally ready: coverage.py can use sys.monitoring to more efficiently measure branch coverage.

I would love for people to try it, but it’s a little involved at the moment:

  • You need to have your own build of Python from the main branch on GitHub, because the CPython side of the work landed after 3.14 alpha 5. Alpha 6 is supposed to arrive within a week, so that will make it easier.
  • I haven’t released a version of coverage.py to PyPI with this code yet, so you also need to install coverage from GitHub:
  • % python3 -m pip install git+https://github.com/nedbat/coveragepy

Once you have both of those things, set the environment variable COVERAGE_CORE=sysmon and run coverage as you usually do. If all goes well, it should be faster. Please let me know!

Feedback is welcome in GitHub issues or in the #coverage-py channel in the Python Discord server.

This has been a long journey, starting when I first commented on PEP 669 that underpins this work. Mark Shannon and I have had many back and forths about the behavior of sys.monitoring, finally landing on something that would work for us both.

For the curious: traditionally coverage.py relied on sys.settrace. Python calls my recording function for every line of Python executed. It’s simple and effective, but inefficient. After I’ve been told a line was executed once, I don’t need to be told again, but settrace keeps calling my function. The new sys.monitoring that arrived in Python 3.12 lets me disable an event once it’s fired, so after the first ping there’s no overhead to running that same code multiple times.

It took a while to iron out the event behavior that lets us measure branches as well as lines, but Python 3.14.0 after alpha 5 has it, so we’re finally able to announce coverage.py support for people to try out.

Intricate interleaved iteration

Thursday 30 January 2025

Someone asked recently, “is there any reason to use a generator if I need to store all the values anyway?” As it happens, I did just that in the code for this blog’s sidebar because I found it the most readable way to do it. Maybe it was a good idea, maybe not. Let me show you.

If you look at the sidebar on the left, “More blog” lists tags and years interleaved in an unusual way: two tags, a year, a tag, a year. That pattern of five repeats:

python
coverage
‘25
my code
‘24
math
beginners
‘23
git
‘22
github
testing
‘21
audio
‘20
(etc)

I chose this pattern because it seemed to fill the space nicely and simpler schemes didn’t look as good. But how to implement it in a convenient way?

Generators are a good way to express iteration (a sequence of values) separately from the code that will consume the values. A simplified version of my sidebar code looks something like this:

def gen_sidebar_links():
    # Get list of commonly used tags.
    tags = iter(list_most_common_tags())
    # Get all the years we've published.
    years = iter(list_all_years())

    while True:
        yield next(tags)
        yield next(tags)
        yield next(years)
        yield next(tags)
        yield next(years)

This nicely expresses the “2/1/1/1 forever” idea, except it doesn’t work: when we are done with the years, next(years) will raise a StopIteration exception, and generators aren’t allowed to raise those so we have to deal with it. And, I wanted to fill out the sidebar with some more tags once the years were done, so it’s actually more like this:

def gen_sidebar_links():
    # Get list of commonly used tags.
    tags = iter(list_most_common_tags())
    # Get all the years we've published.
    years = iter(list_all_years())

    try:
        while True:
            yield next(tags)
            yield next(tags)
            yield next(years)
            yield next(tags)
            yield next(years)
    except StopIteration:   # no more years
        pass

    # A few more tags:
    for _ in range(8):
        yield next(tags)

This relates to the original question because I only use this generator once to create a cached list of the sidebar links:

@functools.cache
def sidebar_links():
    return list(gen_sidebar_links)

This is strange: a generator that’s only called once, and is used to make a list. I find the generator the best way to express the idea. Other ways to write the function feel more awkward to me. I could have built a list directly and the function would be more like:

def sidebar_links():
    # ... Get the tags and years ...

    links = []
    try:
        while True:
            links.append(next(tags))
            links.append(next(tags))
            links.append(next(years))
            links.append(next(tags))
            links.append(next(years))
    except StopIteration:   # no more years
        pass

    for _ in range(8):
        links.append(next(tags))

    return links

Now the meat of the function is cluttered with “links.append”, obscuring the pattern, but could be OK. We could be tricky and make a short helper, but that might be too clever:

def sidebar_links():
    # ... Get the tags and years ...

    use = (links := []).append
    try:
        while True:
            use(next(tags))
            use(next(tags))
            use(next(years))
            use(next(tags))
            use(next(years))
    except StopIteration:   # no more years
        pass

    for _ in range(8):
        use(next(tags))

    return links

Probably there’s a way to use the itertools treasure chest to create the interleaved sequence I want, but I haven’t tried too hard to figure it out.

I’m a fan of generators, so I still like the yield approach. I like that it focuses solely on what values should appear in what order without mixing in what to do with those values. Your taste may differ.

Nat running

Tuesday 14 January 2025

My son Nat is autistic, which makes him quiet and passive. He depends on routines and can be anxious when they vary. We try to prepare him for changes and explain what will happen. He usually watches the world around him seriously.

Because of all that, when he does show us a genuine smile, it is like sunshine bursting through an overcast sky.

I took this picture nine years ago, but it’s still one of my favorites:

Nat, running with a wide grin on his face

Testing some tidbits

Wednesday 4 December 2024

I posted a Python tidbit about checking if a string consists entirely of zeros and ones:

Python expressions checking if a string is only zeros and ones

I got a bunch of replies suggesting other ways. I wanted to post those, but I also wanted to check if they were right. A classic testing structure would have required putting them all in functions, etc, which I didn’t want to bother with.

So I cobbled together a test harness for them (also in a gist if you want):

GOOD = [
    "",
    "0",
    "1",
    "000000000000000000",
    "111111111111111111",
    "101000100011110101010000101010101001001010101",
]

BAD = [
    "x",
    "nedbat",
    "x000000000000000000000000000000000000",
    "111111111111111111111111111111111111x",
    "".join(chr(i) for i in range(10000)),
]

TESTS = """
    # The original checks
    all(c in "01" for c in s)
    set(s).issubset({"0", "1"})
    set(s) <= {"0", "1"}
    re.fullmatch(r"[01]*", s)
    s.strip("01") == ""
    not s.strip("01")

    # Using min/max
    "0" <= min(s or "0") <= max(s or "1") <= "1"
    not s or (min(s) in "01" and max(s) in "01")
    ((ss := sorted(s or "0")) and ss[0] in "01" and ss[-1] in "01")

    # Using counting
    s.count("0") + s.count("1") == len(s)
    (not (ctr := Counter(s)) or (ctr["0"] + ctr["1"] == len(s)))

    # Using numeric tests
    all(97*c - c*c > 2351 for c in s.encode())
    max((abs(ord(c) - 48.5) for c in "0"+s)) < 1
    all(map(lambda x: (ord(x) ^ 48) < 2, s))

    # Removing all the 0 and 1
    re.sub(r"[01]", "", s) == ""
    len((s).translate(str.maketrans("", "", "01"))) == 0
    len((s).replace("0", "").replace("1", "")) == 0
    "".join(("1".join((s).split("0"))).split("1")) == ""

    # A few more for good measure
    set(s + "01") == set("01")
    not (set(s) - set("01"))
    not any(filter(lambda x: x not in {"0", "1"}, s))
    all(map(lambda x: x in "01", s))
"""

import re
from collections import Counter
from inspect import cleandoc

g = {
    "re": re,
    "Counter": Counter,
}

for test in cleandoc(TESTS).splitlines():
    test = test.partition("#")[0]
    if not test:
        continue
    for ss, expected in [(GOOD, True), (BAD, False)]:
        for s in ss:
            result = eval(test, {"s": s} | g)
            if bool(result) != expected:
                print("OOPS:")
                print(f"   {s = }")
                print(f"   {test}")
                print(f"   {expected = }")

It’s a good thing I did this because a few of the suggestions needed adjusting, especially for dealing with the empty string. But now they all work, and are checked!

More Python expressions checking if a string is only zeros and ones

BTW, if you prefer Mastodon to BlueSky, the posts are there too: first and second.

Also BTW: Brian Okken adapted these tests to pytest, showing some interesting pytest techniques.

Dinner

Sunday 1 December 2024

My son Nat has autism, and one way it affects him is he can be very quiet and passive, even when he wants something very much. This played out on our drive home from Thanks­giving this week.

Nat loves his routines, and wants to know what is going to happen. We make him a two-week calendar every weekend, laying out what to expect coming up. Thanks­giving was tricky this year for a few reasons. First, it was especially late in the year. In the early weeks of November, he looked at his calendar and said, “Thursday.” We figured out that he meant, “it’s November, so there should be a special Thursday, but I don’t see it here.” I added on an extra row so he could see when Thanks­giving was going to happen.

But there were other complications. After a few rounds of planning, we ended up with two Thanks­givings: the first on Thursday with my sister, which we’ve hardly ever done, and then a second on Friday with Susan’s family, the usual cohort. We’d be staying in a hotel Thursday night.

We tried to carefully keep Nat informed about the plan and talked about it a number of times. He was great with all of it, all the way through the Friday meal. But driving home Friday night, he seemed a little bothered. We asked him, “what’s wrong?” A common answer to that is “no,” either because he’s not sure how to explain, or he’s not sure he’s allowed to question what’s happening, or some other form of passivity. It’s hard to get an answer because if you offer options (“do your feet hurt?”) he might just repeat that even if it isn’t the real problem.

I thought maybe he was concerned about what was going to be happening next, and often going over the routine helps. So we started to review the plan. I asked, “where are we sleeping tonight?” He answered “Brookline.”

“Tomorrow where will you eat breakfast?” — “Brookline.”

“Where will you eat lunch?” — “Brookline.”

“Where will you eat dinner?” — Here we expected he’d name his group home, but instead he said — “dinner.”

Aha! This was the clue we needed. Here’s another tricky thing about Thanks­giving: if you have a meal at 4pm (as we had on Thursday), that counts as dinner. But what if you have a meal at 2pm as we had on Friday? Even if it’s a large meal and you aren’t hungry, by the time it gets dark shouldn’t there be dinner? We didn’t have dinner! This was what was bothering him. We had completely skipped over part of the expected routine. And even with all our planning, we hadn’t thought to explain that Grandma’s big Friday meal was going to be both lunch and dinner.

So we asked, “Do you want to stop somewhere to have dinner?” — “Yes.” So we stopped at McDonald’s for a crispy chicken sandwich (removed from the bun, dipped in sweet & sour sauce), fries and a Sprite. Judging from the noises once we were back in the car, maybe he was stuffing himself on principle, but we were back on the routine, so everyone was happy.

It’s not easy to find out what Nat wants, so when he tells us, even indirectly, we try to give it to him. Some people might have resisted making a stop when we were already late getting home, or having a meal when no one was actually hungry. But it wasn’t difficult and didn’t take long. It was a small thing to do, but felt like a large part of parenting: listening to your children’s needs no matter how quiet, and helping to meet them even when they are different than your own.

Older: