Coverage 4.5

Sunday 4 February 2018

Just out: coverage.py v4.5.

There’s one new feature: configurator plug-ins, that let you run Python code at startup to set the configuration for coverage. This side-steps a requested feature to have different exclusion pragmas for different versions of Python.

People wanted to be able to say, this line of code is excluded from coverage when run under Python 3, but not under Python 2. That sounds simple enough, but then some wanted to be able to exclude for Python 3.5, but not 3.6. Or, excluded when running under PyPy, but not under CPython.

I could see this turning into a never-ending road of finer and finer differentiation. Next would be operating systems, or versions of Django, or, etc, etc, etc.

Rather than me building all that into coverage itself, now you can write your own plug-in that makes all those determinations, and sets the exclude pragmas as you like.

Python’s misleading readability

Tuesday 23 January 2018

One of the things that has made Python successful is its readability. Code is clear and easy to understand. One of the reasons is that Python uses words for a few things that other languages use symbols for. But sometimes the readability is misleading. Beginners construct valid Python expressions that don’t do what they seem like they should do.

Let’s say you want to know if your variable x is equal to 17. You could do:

if x is 17:

This might work. But then if you try:

if name is "Ned":

it doesn’t work? What!? Why not? It’s so clear.

The problem is that “is” doesn’t check two values for equality, it checks if the left and right side are precisely the same object. But you can have two different string objects, each of which has the value “Ned”. You don’t use “is” to check equality, you use “==”:

if name == "Ned":

It’s not just strings: numbers can also do surprising things:

>>> 1000 + 1 is 1001
False

“x is 17” is more English-like than “x == 17”, but it isn’t right. This is one time that Python’s famed readability leads you to the wrong construct.

Another example: you need to know if the answer was either “y” or “yes”, so you try this:

if answer == "y" or "yes":
    print("Thanks")

and now your program doesn’t work. No matter what answer is, it prints “Thanks.” Why?

The “or” operator is for combining boolean (true/false) expressions. The result is true if either of its operands is true. So your code is equivalent to:

if (answer == "y") or ("yes"):
    print("Thanks")

If answer is “y”, then the if will be true. If answer isn’t “y”, then the or will consider the right-hand side, “yes”. Strings are true if they are not empty, so “yes” is always true. So the if condition will always be true, no matter what value answer has.

The right ways to do this are:

if answer == "y" or answer == "yes":
    print("Thanks")

or if you want to be fancier,

if answer in {"y", "yes"}:
    print("Thanks")

(a list or a tuple would also work here instead of a set, though then you get into philosophical debates about how many data structures fit on the head of a pin.)

Don’t get me wrong, I agree that Python is very readable. And every language has constructs that seem like they should work, but don’t. You have to study well, and be careful to use your chosen language properly.

2018

Tuesday 2 January 2018

When I was in high school, I was not a diligent student of English. I would try to do the reading on the subway ride, and not very well. One morning, I was on page 38 when I got to my stop. I didn’t have a bookmark, and I didn’t like to turn down the corners of pages, so to remember my place, I thought about what was interesting about the number 38.

Hmm, 38 is twice a prime. It’s one more than 37, which is a prime. Oh, and 39 is three times a prime. Interesting: a sequence of three consecutive numbers that are respectively 1-times, 2-times, and 3-times a prime. BTW, it worked: to this day, I remember what page I was on!

Then a recent tweet brought back this interesting confluence: 2018 has the same property. 2017 is a prime, 2018 is twice a prime, and 2019 is three times a prime.

It turns out this property is rare. There are only eleven numbers less than 3000 like this: 14, 38, 158, 542, 878, 1202, 1382, 1622, 2018, 2558, 2858; and only 541 such numbers less than 1,000,000.

So 2018 is a truly unusual year. 2017 was difficult politically, but there are hopeful signs of a turning tide. Here’s to the year ahead, and fighting the good fights: personally, locally, and nationally.

Iter-tools for puzzles: oddity

Friday 8 December 2017

It’s December, which means Advent of Code is running again. It provides a new two-part puzzle every day until Christmas. They are a lot of fun, and usually are algorithmic in nature.

One of the things I like about the puzzles is they often lend themselves to writing unusual but general-purpose helpers. As I have said before, abstraction of iteration is a powerful and under-used feature of Python, so I enjoy exploring it when the opportunity arises.

For yesterday’s puzzle I needed to find the one unusual value in an otherwise uniform list. This is the kind of thing that might be in itertools if itertools had about ten times more functions than it does now. Here was my definition of the needed function:

def oddity(iterable, key=None):
    """
    Find the element that is different.

    The iterable has at most one element different than the others. If a
    `key` function is provided, it is a function used to extract a comparison
    key from each element, otherwise the elements themselves are compared.

    Two values are returned: the common comparison key, and the different
    element.

    If all of the elements are equal, then the returned different element is
    None.  If there is more than one different element, an error is raised.

    """

The challenge I set for myself was to implement this function in as general and useful a way as possible. The iterable might not be a list, it could be a generator, or some other iterable. There are edge cases to consider, like if there are more than two different values.

If you want to take a look, My code is on GitHub (with tests, natch.) Fair warning: that repo has my solutions to all of the Advent of Code problems so far this year.

One problem with my implementation: it stores all the values from the iterable. For the actual Advent of Code puzzle, that was fine, it only had to deal with less than 10 values. But how would you change the code so that it didn’t store them all?

My code also assumes that the comparison values are hashable. What if you didn’t want to require that?

Suppose the iterable could be infinite? This changes the definition somewhat. You can’t detect the case of all the values being the same, since there’s no such thing as “all” the values. And you can’t detect having more than two distinct values, since you’d have to read values forever on the possibility that it might happen. How would you change the code to handle infinite iterables?

These are the kind of considerations you have to take into account to write truly general-purpose itertools functions. It’s an interesting programming exercise to work through each version would differ.

BTW: it might be that there is a way to implement my oddity function with a clever combination of things already in itertools. If so, let me know!

Bite-sized command line tools: pylintdb

Sunday 3 December 2017

One of the things I love about Python is the abundance of handy libraries to cobble together small but useful tools. At work we had a large pylint report, and I wanted to understand it better. In particular, I wanted to trace back to which commit had introduced the violations. I wrote pylintdb.py to do the work.

Since we had a lot of violations (>5000!) I figured it would take some time to use git blame to find the commit for each line. I wanted a way to persist the progress through the lines. SQLite seemed like a good choice. It also would give me ad-hoc queryability, though to be honest, I didn’t even consider that at the time.

SQLite is part of the Python standard library, but there’s a third-party library that makes it super-convenient to use. Dataset lets you use a database without creating a schema or even model first. You just open a database, choose a table name, and then start writing dictionaries to it. It handles all the schema creation (or modification!) behind the scenes. Awesome.

These days, click is the tool of choice for command-line parsing, and other chores needed in the terminal. I used the progress bar functions. They aren’t perfect, but in only a few lines I had a workable indicator.

Other useful things from the Python standard library:

  • concurrent.futures for parallelizing the git blame work. It’s got a high-level “map” interface that did exactly what I needed without having to think about queues, threads, and so on.
  • subprocess.check_output does the subprocess thing people usually want: just run the command and give me the output.

pylintdb isn’t earth-shattering, it just does exactly what I needed in 120 lines with a minimum of fuss, thanks to dataset, click, and Python.

New design

Saturday 25 November 2017

I’ve just updated the design of this site. The goal was to make it responsive, so that it would work well on small screens, but I made other changes along the way. The body type is now serif rather than sans, and much larger. I made lots of other tweaks as I worked on pages.

Making a responsive design was fun: it meant working out mechanisms for the layout rather than just a static design.

Of course, it’s easy to get carried away. Take a look at what happens to my name in the header when the screen gets below 300 pixels: Ned Batchelder becomes nedbat to save space. This was accomplished with the help of a span with class “chelder”...

It took me a long time to make this design. I started it 15 months ago, but stopped work on it for more than a year. I picked it up again two weeks ago, and powered through the remaining work.

Behind the scenes, I changed only one thing: using Sass to generate the CSS. The rest is still as janky and difficult as always.

For comparison (and posterity), here is the design I just replaced. If anything seems amiss with the new design, just let me know.

Older: