On the relative readability of Perl and Python

In a recent post, our own Buddy Burden expounds on the relative merits of readability vs flexibility (in the guise of Python vs Perl respectively). While he does not completely concede the point that Python is more readable than Perl, I will go so far as to negate it. In fact, I would say that the prettiness of Python can even mask some real readability issues.

I have been a Perl programmer for several years, but early this summer took a job writing Python. I’m glad I’ve learned it, there are several things that I would love to see added to Perl, most notably a set type and a proper in operator (~~ got close). That said I will absolutely not concede that Python is easier to read than Perl, as all too often is taken as the axiom that many believe it is.

And I have examples.

Let me get out a few nits first. My biggest pet peeve, though it has never really bit me yet, is that there are two syntactic uses of a trailing comma! Meaning times when a trailing comma actually changes the meaning of the statement. When printing, a trailing comma prevents a newline from being printed (ick!) and worse when constructing a tuple (immutable list object, created by round parens), and specifically one with only one item, don’t forget that trailing comma, or the parens are precedence delimiters and not a tuple constructor (yuck!). This just hurts me because I think that languages that don’t allow a trailing comma are painful, and so I appreciate when a language does allow it. That said, it should never mean anything, its just to help keep your diffs nice!

Also, why rearrange the ternary from:

test ? 'value if true' : 'value if false'

to:

 'value if true' if test else 'value if false'

Yes its just style, but my beef is that it’s no prettier, its just different for the sake of being different. Oh and no ++ operator. Why use one statement when you could have two (maybe three if there is a conditional involved (try writing a python version of: say $i++ if $test)). Enough said.

On to the bad things. Lacking braces, code blocks are denoted by indentation, as you are likely aware. When writing lots of real world code though, you have to nest the blocks. Say class definition, with a method definition, which has an if statement and closure. Now say you have several of these, and maybe the outer section goes off the top of your screen. Can you tell, just by looking, how deeply indented you are? Are you sure? Be careful too, because variables aren’t block scoped nor declared. Did that indentation level just torpedo your code? Well there is no compile-time checking, so let’s hope for no runtime errors.

Let’s pause for a minute and ponder the syntactic whitespace. Why do we like it? It seems to be cleaner without all those ugly braces, but, then again, you still need that : and probably a newline, that’s the same number of characters. And please, oh please, do not intermingle your whitespace types. The interpreter can’t handle it, and you can’t see it. Happy debugging. Did your editor do that? Did your collaborator with the funny IDE? Now back to your regularly scheduled rant.

Then there are … “List Comprehensions”. Rather than some sane ordering of statements to build a list, like properly flowing map/greps in Perl you might get something like this:

values = [ item.value for item in list if item.value > 4 and item.name != 'bob']

The iterator is in the middle, the map is in the front, the grep(s) are in the back … its not easy to find anything, and worse to try to lay out clearly. I would write the same thing in Perl as:

my @values =
  map { $_->value }
  grep { $_->value > 4 && $_->name ne 'bob' } 
  @items;

See how being able (indeed encouraged) to employ better line usage, and braces, helps here; your command is now a flow of distinct transforms, rather than jumbled statements. But now, what if we need, (gasp!), nested list comprehensions!

  values = [ item.value for bucket in buckets for item in bucket ]

Why is the second iterator before the first? And please don’t ask me where the conditionals for the filters go for each level in there! If you can figure that out, then you are better than I. Also where does exception handling, or special casing go? Outside in, generators of course. (Ever wonder why Python loves generators and they seem cool, but somehow you never need them in Perl? It’s at least partially because we don’t use this construct.).

The odd thing is, though, for a language that prides itself on clarity, you would think that the use of this horrible construct would be discouraged; yet I typically see it touted as a strength! Really?! This isn’t something a nice map/grep (and maybe even sort to be cute) couldn’t top nicely?

Finally my number one beef. Almost all the previous combine to form this cluster. Methods are just attributes that you can call. So please don’t scroll too far, forget something, or miss a nesting level, or confuse a name, and assign to an attribute which is already a method. There goes your method. No warning, no help, no checking, no sigils. You just broke the system. That nice, maintainable, readable system.

Sure it looks pretty. But did you see that error get made, Mr. Maintainer. I bet you didn’t.

Thanks, but I’ll stick to my beautiful language Perl. It takes a little longer to appreciate, I’ll grant you. But really take another look.

18 Comments

That said I will absolutely not concede that Python is easier to read than Python

I don’t see how it can be.

Working in a mixed Perl/Python shop, I find a number of things that don’t work well in both languages.

One of the odder Python issues comes from our style guideline of lines less than 80 columns long. Nested indents, even with line continuations, often get really hairy. This is a shame, as 160 is easily handled, and is only a small problem when doing side-by-side diffs. (Over/under is available too.)

We do grouse a bit about some Perl issues, most of which come from pre-Perl 5.0 behaviors, and will be very hard to scrub from the language. For instance, the differing behavior of eof, with and without parens. Or the lack of a name slot for “anonymous” code blocks, when they don’t need to be anonymous, but only created at runtime. Or the difficulty of finding a serializer that can report the code from a coderef (even if just for identification purposes in debug). (There’s probably some way to feed coderefs to a deparser and get something useful back, but I haven’t found it, nor looked in anger.)

My colleagues often get snagged by the sigil/bracket trap, such as accidentally making an arrayref and assigning it to the first element of an array when the intent was just to fill a 1D array; or accidentally making a hashref instead of a codeblock.

My own opinion is that good Perl is somewhat easier to read than good Python, but I would color that with my shallower experience with Python, getting distracted by syntax, list comprehensions (who came up with that crap name? — “list generator” seems clearer), rampant whitespace, and as you say, trailing commas. Perl seems a bit more natural, though because of it’s flexibility it can be abused to a greater extent as well.

The love of Python

print '3' > 2    # True
print 3 > '2'    # False

(I know it is fixed in Python3, which is only used by approx. 2% of the users)

local and global variables

a = 42
def f():
    print('and the answer is:')
    print(a)

f()

Works and prints

print('and the answer is:
42

But

a = 42
def f():
    print('and the answer is:')
    print(a)
    a = 42

f()

Blows up during run-time:

and the answer is:
Traceback (most recent call last):
  File "a.py", line 7, in 
    f()
  File "a.py", line 4, in f
    print(a)
UnboundLocalError: local variable 'a' referenced before assignment

In a nutshell: Python has a fair amount of strange behavior.

@QM: For debugging, I think you want Data::Dump::Streamer if you want to be able to dump the absolute maximum of Perl data structures including coderefs. It optionally comes with a shortcut name “DDS”:

perl -MDDS -e 'sub foo { my $x = shift; return 2*$x; }Dump(\&foo)'

$CODE1 = sub {
           my $x = shift();
           return 2 * $x;
         };

QM, here are some resources for you:

Or the lack of a name slot for “anonymous” code blocks, when they don’t need to be anonymous, but only created at runtime.

You can use local *__ANON__ = ‘name_of_anonymous_sub’ for that:


    use Carp::Always;
    my $sub = sub {
        local *__ANON__ = 'this is an anonymous sub';
        die;
    };
    $sub->();

That prints:


    Died at test.pl line 4.
    main::this is an anonymous sub() called at test.pl line 6

I’ve offered to document this, but there wasn’t consensus on this.

Or the difficulty of finding a serializer that can report the code from a coderef (even if just for identification purposes in debug).

I wrote Sub::Information to bundle together tons of useful subroutine modules into one module. To solve your problem:


    use Sub::Information 'inspect';
    sub add_2 { return 2 + shift }
    print inspect(\&add_2)->code;

    __END__
    # output
    $CODE1 = sub {
      use strict 'refs';
      return 2 + shift(@_);
    };

Indentation-based scope plus implicit variable declarations plus no checks until runtime bit me several times the last time had significant contact with Python, on a mid-sized Django project. (It didn’t help that the previous developers had slightly different indentation styles.) It’s especially painful for refactorings like extracting or inlining methods. Cut a chunk of code in one place, paste it in another, and things can go wrong in so many ways as you re-indent the thing to conform to its new environment. “C-y C-M-"? Ha ha, you wish! You get to reindent every line by hand, and if you mis-nest things somewhere, you won’t find out until the function actually runs, if you’re lucky.

I agree to most of what you’re saying, however to me this Python construct is much easier to understand than your Perl equivalent using grep and map:

values = [ item.value for item in list if item.value > 4 and item.name != 'bob']

FYI: You actually have an error in your Perl construct, “item” is not defined in “item->name”: my @values = map { $->value } grep { $->value > 4 && item->name != 'bob'} @items;

I worked in 3 companies that have large Perl code bases (and I’m happy to be using Perl), but map and grep were never used because they’re not easy to understand. We never had any formal rule against them, but people would just write the same code using foreach statements. And this is where a simple Python list comprehension would come in very handy.

thetrb: Having gone through the “ugh, map and grep are so ugly” phase myself, I guarantee you it’s a training issue. I fixed that defect in my perl training on my own by reading the first few chapters of SICP. That book made map, grep and so many other things click very nicely.

List comprehensions are amazing constructs in both python and Erlang. I really miss them in Perl.

Why? Just saying that you like them in an in-depth discussion of them doesn’t contribute. Please give some real-life examples of things where comprehensions would’ve resulted in nicer code in perl.

I’m so exhausted from the lack of readability of modern Functional programming (recent projects in D3.js and Scala), I cant consciously complain about these two old friends.

Perl gives me more ways to write the same thing, so I get to choose which is the most readable.

Thanks Ovid and Steffen Mueller for the anonymous sub tips. Some of these came up on http://www.perlmonks.org/?nodeid=1048128, with the goal of generating stack traces to report exceptions, where many callbacks are created on the fly. The local *ANON trick doesn’t affect svref2object returns.

There is resistance to adding XS modules to all of our systems (there’s even resistance to adding pure Perl, though not as much).

Next time it comes around, I would revisit the ideas brian puts forth in http://www.effectiveperlprogramming.com/2011/09/enchant-closures-for-better-debugging-output/, as that will have less internal resistance and many benefits. One plus is that the enchanted closures can be tweaked for debugging purposes with little effort.

In several places above there is still a small typo: != ‘bob’ should be ne ‘bob’.

You can always do if test: i += 1 , no newline required. This is about the same as the Perl example.

For list comprehensions, nothing is preventing you from using line breaks to make things more readable:


values = [x.value 
          for x in list
          if x.value > 4 and x.name != 'bob']

Leave a comment

About Joel Berger

user-pic As I delve into the deeper Perl magic I like to share what I can.