Skip to Content

Some pylint tips

I’ve been using pylint for quite some time now, so today I’d like to share a few tips with you.

Update: Itamar Turner-Trauring also wrote a nice article about pylint, you should read it too: Why Pylint is both useful and unusable, and how you can actually use it

What is pylint? #

It’s a static analyzer for Python code. “Static” means that it won’t execute your code, it will just parse it to find mistakes or things that do not respect a given coding style.

Pylint is capable of emitting very interesting warnings.

Here are some examples:

def foo(bar, baz):
    pass

foo(42)
No value for argument 'baz' in function call (no-value-for-parameter)

class MyThread(threading.Thread):
    def __init__(self, name):
        self.name = name

    def run(self):
        ...


my_thread = MyThread()
my_thread.start()
__init__ method from base class 'Thread' is not called (super-init-not-called)

This is nice because it saves you a fatal assertion at runtime:

AssertionError: Thread.__init__() not called

Fixing pylint output #

By default, pylint is very verbose:

$ pylint my_module.py
No config file found, using default configuration
************* Module my_module
C:  1, 0: Missing module docstring (missing-docstring)
...

Report
======
8 statements analysed.

Statistics by type
------------------

+---------+-------+-----------+-----------+------------+---------+
|type     |number |old number |difference |%documented |%badname |
+=========+=======+===========+===========+============+=========+
|module   |1      |1          |=          |0.00        |100.00   |
+---------+-------+-----------+-----------+------------+---------+
...
+---------+-------+-----------+-----------+------------+---------+


Raw metrics
-----------

...

Messages by category
--------------------

+-----------+-------+---------+-----------+
|type       |number |previous |difference |
+===========+=======+=========+===========+
|convention |3      |3        |=          |

...

+-----------+-------+---------+-----------+
|error      |1      |1        |=          |
+-----------+-------+---------+-----------+


Global evaluation
-----------------
Your code has been rated at -3.75/10 (previous run: 1.43/10, -5.18)

Let’s fix that!

The goal is to have no output at all when everything is fine, only have the errors if something is wrong, and make sure output of pylint can then be used by an other program if required. 1

Get rid of “No config file found, using default configuration” #

Just go to the root of your project and run:

$ pylint --generate-rcfile > pylintrc

More readable output #

Edit the pylintrc file to have:

[REPORTS]

output-format=parseable

That way you’ll get a more standard output, with the file name, a colon, the line number and the error message:

my_module.py:11: [C0103(invalid-name), ] Invalid constant name "my_thread"

This is useful if you want to run pylint from your editor and quickly jump to the lines that contains errors.

Get rid of the useless stuff #

You don’t really care about all the stats, so let’s just disable everything:

[REPORTS]

reports = no

There! Now we only get only the warning or error messages, except there’s a big line to separate the modules:

************* Module my_module
my_module.py:4: [W0231(super-init-not-called), MyThread.__init__] __init__ method from base
                class 'Thread' is not called
************* Module other_stuff
other_stuff.py:5: [W0311(bad-indentation), ] Bad indentation. Found 3 spaces, expected 4

Correcting false positives #

Sometimes pylint thinks there’s a problem in your code even though it’s perfectly fine.

Here’s an example using the excellent path.py library:

my_path = path.Path(".").abspath()
my_path.joinpath("foo")
my_module.py:5: [E1120(no-value-for-parameter), main] No value for argument 'first' in unbound method call

If you take a look, it seems that pylint gets confused by upstream code:

class multimethod(object):
    """
    Acts like a classmethod when invoked from the class and like an
    instancemethod when invoked from the instance.
    """
    ...


class Path():
    ...

    @multimethod
    def joinpath(cls, first, *others):
        if not isinstance(first, cls):
            first = cls(first)
        return first._next_class(first.module.join(first, *others))

It’s some dark magic (yeah Python) to make sure you can use both:

path_1 = my_path.joinpath("bar")
path_2 = path.Path.joinpath(my_path, "bar")

and pylint only “gets” the second usage…

Here the solution is to use a “pragma” to tell pylint that the code is fine

my_path = path.Path(".").abspath()
# pylint: disable=no-value-for-parameter
a_path = my_path.joinpath("foo")

But if you do that, you’ll get:

my_module.py:5: [I0011(locally-disabled), ] Locally disabling no-value-for-parameter (E1120)

The solution is to disable warnings about disabled warnings (so meta):

[MESSAGES CONTROL]

disable=reduce-builtin,dict-iter-method,reload-builtin, ... ,locally-disabled

Freezing pylint version #

If you’re like me, you probably have a dev-requirements.txt containing a line about pylint in order to use in a virtualenv.

It’s also possible you’re using buildout.

But anyway, I highly recommend you have a separate installation of pylint just for your project, isolated from the rest of your system.

The fact is that pylint depends on astroid, and both projects are constantly evolving.

So if you’re not careful, you may end up upgrading astroid or pylint and suddenly some false positives will get fixed, and some other will appear.

So to make sure this does not happen, always freeze pylint and astroid version numbers, like so:

pylint==1.5.5
astroid==1.4.7

(you can use pip freeze to see the version of all the packages in your virtualenv)

Running pylint #

To run pylint, you have to give it a list of packages or modules do check on the command line.

For instance, let’s assume your sources look like this:

src
  pylintrc
  bar.py
  spam
     __init__.py
     eggs.py

Then you have to call pylint like this:

$ cd src
$ pylint bar.py spam

You may try to run pylint . or just pylint but it won’t work :/

So this means that anytime you add a package or a new module, you have to change the way you call pylint.

This is rather annoying, that’s why I suggest you use invoke.

Write a tasks.py file looking like:

import path
import invoke

def get_pylint_args():
    top_path = path.Path(".")
    top_dirs = top_path.dirs()
    for top_dir in top_dirs:
        if top_dir.joinpath("__init__.py").exists():
            yield top_dir
    yield from (x for x in top_path.files("*.py"))


@invoke.task
def pylint():
    invoke.run("pylint " + " ".join(get_pylint_args()), echo=True)

And then you just have to use:

$ invoke pylint

Speeding up pylint #

pylint also knows how to use multiple jobs so that it runs faster.

Since you are already using tasks.py, you can specify the number of jobs to use in a cross-platform way easily:

import multiprocessing


def get_pylint_args():
    ...

    num_cpus = multiprocessing.cpu_count()
    yield "-j%i" % num_cpus

Shorter version #

You can also not do all of the above and just fire up pylint like so:

$ pylint -E *.py

It will only show errors, (-E is short for --errors-only), and will have a good enough output.

  • Pros: you don’t have to write any pylintrc file
  • Cons: you may hide serious bugs

That’s all for today, see you next time!


  1. Congrats if you noticed this is part of the Unix philosophy ↩︎


Thanks for reading this far :)

I'd love to hear what you have to say, so please feel free to leave a comment below, or read the contact page for more ways to get in touch with me.

Note that to get notified when new articles are published, you can either:

Cheers!