{"id":67306,"date":"2022-09-19T09:01:06","date_gmt":"2022-09-19T09:01:06","guid":{"rendered":"https:\/\/www.cryptocabaret.com\/?p=67306"},"modified":"2022-09-19T09:01:06","modified_gmt":"2022-09-19T09:01:06","slug":"pylint-the-good-the-bad-and-the-ugly","status":"publish","type":"post","link":"https:\/\/www.cryptocabaret.com\/?p=67306","title":{"rendered":"PyLint: The good, the bad, and the ugly"},"content":{"rendered":"<p><span class=\"field field--name-title field--type-string field--label-hidden\">PyLint: The good, the bad, and the ugly<\/span><br \/>\n<span class=\"field field--name-uid field--type-entity-reference field--label-hidden\"><a title=\"View user profile.\" href=\"https:\/\/opensource.com\/users\/moshez\" class=\"username\">Moshe Zadka<\/a><\/span><br \/>\n<span class=\"field field--name-created field--type-created field--label-hidden\">Mon, 09\/19\/2022 &#8211; 03:00<\/span><\/p>\n<div class=\"clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item\">\n<p>Hot take: PyLint is actually good!<\/p>\n<p>&#8220;PyLint can save your life&#8221; is an exaggeration, but not as much as you might think! PyLint can keep you from really really hard to find and complicated bugs. At worst, it can save you the time of a test run. At best, it can help you avoid complicated production mistakes.<\/p>\n<h2>The good<\/h2>\n<p>I&#8217;m embarrassed to say how common this can be. Naming tests is perpetually <em>weird<\/em>: Nothing cares about the name, and there&#8217;s often not a natural name to be found. For instance, look at this code:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"python geshifilter-python\"><span class=\"kw1\">def<\/span> test_add_small<span class=\"br0\">(<\/span><span class=\"br0\">)<\/span>:<br>\n\u00a0 \u00a0 <span class=\"co1\"># Math, am I right?<\/span><br>\n\u00a0 \u00a0 <span class=\"kw1\">assert<\/span> <span class=\"nu0\">1<\/span> + <span class=\"nu0\">1<\/span> <span class=\"sy0\">==<\/span> <span class=\"nu0\">3<\/span><br>\n\u00a0 \u00a0 <br><span class=\"kw1\">def<\/span> test_add_large<span class=\"br0\">(<\/span><span class=\"br0\">)<\/span>:<br>\n\u00a0 \u00a0 <span class=\"kw1\">assert<\/span> <span class=\"nu0\">5<\/span> + <span class=\"nu0\">6<\/span> <span class=\"sy0\">==<\/span> <span class=\"nu0\">11<\/span><br>\n\u00a0 \u00a0 <br><span class=\"kw1\">def<\/span> test_add_small<span class=\"br0\">(<\/span><span class=\"br0\">)<\/span>:<br>\n\u00a0 \u00a0 <span class=\"kw1\">assert<\/span> <span class=\"nu0\">1<\/span> + <span class=\"nu0\">10<\/span> <span class=\"sy0\">==<\/span> <span class=\"nu0\">11<\/span><\/div><\/div><\/pre>\n<p>The test works:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"python geshifilter-python\">collected <span class=\"nu0\">2<\/span> items \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <br><span class=\"kw3\">test<\/span>.<span class=\"me1\">py<\/span> .. <br><span class=\"nu0\">2<\/span> passed <\/div><\/div><\/pre>\n<\/p>\n<div class=\"embedded-resource-list callout-float-right\">\n<div class=\"field field--name-title field--type-string field--label-hidden field__item\">More Python resources<\/div>\n<div class=\"field field--name-links field--type-link field--label-hidden field__items\">\n<div class=\"field__item\"><a href=\"https:\/\/www.redhat.com\/en\/topics\/middleware\/what-is-ide?intcmp=7016000000127cYAAQ\">What is an IDE?<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/downloads\/cheat-sheet-python-37-beginners?intcmp=7016000000127cYAAQ\">Cheat sheet: Python 3.7 for beginners<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/resources\/python\/gui-frameworks?intcmp=7016000000127cYAAQ\">Top Python GUI frameworks<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/downloads\/7-essential-pypi-libraries?intcmp=7016000000127cYAAQ\">Download: 7 essential PyPI libraries<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/developers.redhat.com\/?intcmp=7016000000127cYAAQ\">Red Hat Developers<\/a><\/div>\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/tags\/python?intcmp=7016000000127cYAAQ\">Latest Python articles<\/a><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<p>But here&#8217;s the kicker: If you override a name, the testing infrastructure happily skips over the test!<\/p>\n<p>In reality, these files can be hundreds of lines long, and the person adding the new test might not be aware of all the names. Unless someone is looking at test output carefully, everything looks fine.<\/p>\n<p>Worst of all, the <em>addition of the overriding test<\/em>, the <em>breakage of the overridden test<\/em>, and the <em>problem that results in prod<\/em> might be separated by days, months, or even years.<\/p>\n<h2>PyLint finds it<\/h2>\n<p>But like a good friend, PyLint is there for you.<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"python geshifilter-python\"><span class=\"kw3\">test<\/span>.<span class=\"me1\">py<\/span>:<span class=\"nu0\">8<\/span>:<span class=\"nu0\">0<\/span>: E0102: function already defined line <span class=\"nu0\">1<\/span><br>\n\u00a0 \u00a0 \u00a0<span class=\"br0\">(<\/span>function-redefined<span class=\"br0\">)<\/span><\/div><\/div><\/pre>\n<h2>The bad<\/h2>\n<p>Like a 90s sitcom, the more you get into PyLint, the more it becomes problematic. This is completely reasonable code for an inventory modeling program:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"python geshifilter-python\"><span class=\"st0\">\"\"\"Inventory abstractions\"\"\"<\/span><br><br><span class=\"kw1\">import<\/span> attrs<br><br><span class=\"sy0\">@<\/span>attrs.<span class=\"me1\">define<\/span><br><span class=\"kw1\">class<\/span> Laptop:<br>\n\u00a0 \u00a0 <span class=\"st0\">\"\"\"A laptop\"\"\"<\/span><br>\n\u00a0 \u00a0 ident: <span class=\"kw2\">str<\/span><br>\n\u00a0 \u00a0 cpu: <span class=\"kw2\">str<\/span><\/div><\/div><\/pre>\n<p>It seems that PyLint has opinions (probably formed in the 90s) and is not afraid to state them as facts:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"python geshifilter-python\">$ pylint laptop.<span class=\"me1\">py<\/span> | sed -n <span class=\"st0\">'\/^laptop\/s\/[^ ]*: \/\/p'<\/span><br>\nR0903: Too few public methods <span class=\"br0\">(<\/span><span class=\"nu0\">0<\/span>\/<span class=\"nu0\">2<\/span><span class=\"br0\">)<\/span> <span class=\"br0\">(<\/span>too-few-public-methods<span class=\"br0\">)<\/span><\/div><\/div><\/pre>\n<h2>The ugly<\/h2>\n<p>Ever wanted to add your own unvetted opinion to a tool used by millions? PyLint has 12 million monthly downloads.<\/p>\n<blockquote>\n<p>&#8220;People will just disable the whole check if it&#8217;s too picky.&#8221; \u2014PyLint issue 6987, July 3rd, 2022<\/p>\n<\/blockquote>\n<p>The attitude it takes towards adding a test with potentially many false positives is&#8230;<em>&#8220;eh.&#8221;<\/em><\/p>\n<h2>Making it work for you<\/h2>\n<p>PyLint is fine, but you need to interact with it carefully. Here are the three things I recommend to make PyLint work for you.<\/p>\n<h3>1. Pin it<\/h3>\n<p>Pin the PyLint version you use to avoid any surprises!<\/p>\n<p>In your <code>.toml<\/code> file:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"ini geshifilter-ini\"><span class=\"re0\"><span class=\"br0\">[<\/span>project.optional-dependencies<span class=\"br0\">]<\/span><\/span><br><span class=\"re1\">pylint<\/span> <span class=\"sy0\">=<\/span><span class=\"re2\"> <span class=\"br0\">[<\/span><\/span><span class=\"st0\">\"pylint\"<\/span><span class=\"br0\">]<\/span><\/div><\/div><\/pre>\n<p>In your code:<\/p>\n<pre>\n<span class=\"geshifilter\"><code class=\"python geshifilter-python\"><span class=\"kw1\">from<\/span> <span class=\"kw3\">unittest<\/span> <span class=\"kw1\">import<\/span> mock<\/code><\/span><\/pre>\n<p>This corresponds with code like this:<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"python geshifilter-python\"><span class=\"co1\"># noxfile.py<\/span><br>\n...<br><span class=\"sy0\">@<\/span>nox.<span class=\"me1\">session<\/span><span class=\"br0\">(<\/span>python<span class=\"sy0\">=<\/span>VERSIONS<span class=\"br0\">[<\/span>-<span class=\"nu0\">1<\/span><span class=\"br0\">]<\/span><span class=\"br0\">)<\/span><br><span class=\"kw1\">def<\/span> refresh_deps<span class=\"br0\">(<\/span>session<span class=\"br0\">)<\/span>:<br>\n\u00a0 \u00a0 <span class=\"st0\">\"\"\"Refresh the requirements-*.txt files\"\"\"<\/span><br>\n\u00a0 \u00a0 session.<span class=\"me1\">install<\/span><span class=\"br0\">(<\/span><span class=\"st0\">\"pip-tools\"<\/span><span class=\"br0\">)<\/span><br>\n\u00a0 \u00a0 <span class=\"kw1\">for<\/span> deps <span class=\"kw1\">in<\/span> <span class=\"br0\">[<\/span>...<span class=\"sy0\">,<\/span> <span class=\"st0\">\"pylint\"<\/span><span class=\"br0\">]<\/span>:<br>\n\u00a0 \u00a0 \u00a0 \u00a0 session.<span class=\"me1\">run<\/span><span class=\"br0\">(<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <span class=\"st0\">\"pip-compile\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <span class=\"st0\">\"--extra\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 deps<span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <span class=\"st0\">\"pyproject.toml\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <span class=\"st0\">\"--output-file\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 f<span class=\"st0\">\"requirements-{deps}.txt\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 <span class=\"br0\">)<\/span><\/div><\/div><\/pre>\n<h3>2. Default deny<\/h3>\n<p>Disable all checks. Then enable ones that you think have a high value-to-false-positive ratio. (Not just false-negative-to-false-positive ratio!)<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"python geshifilter-python\"><span class=\"co1\"># noxfile.py<\/span><br>\n...<br><span class=\"sy0\">@<\/span>nox.<span class=\"me1\">session<\/span><span class=\"br0\">(<\/span>python<span class=\"sy0\">=<\/span><span class=\"st0\">\"3.10\"<\/span><span class=\"br0\">)<\/span><br><span class=\"kw1\">def<\/span> lint<span class=\"br0\">(<\/span>session<span class=\"br0\">)<\/span>:<br>\n\u00a0 \u00a0 files <span class=\"sy0\">=<\/span> <span class=\"br0\">[<\/span><span class=\"st0\">\"src\/\"<\/span><span class=\"sy0\">,<\/span> <span class=\"st0\">\"noxfile.py\"<\/span><span class=\"br0\">]<\/span><br>\n\u00a0 \u00a0 session.<span class=\"me1\">install<\/span><span class=\"br0\">(<\/span><span class=\"st0\">\"-r\"<\/span><span class=\"sy0\">,<\/span> <span class=\"st0\">\"requirements-pylint.txt\"<\/span><span class=\"br0\">)<\/span><br>\n\u00a0 \u00a0 session.<span class=\"me1\">install<\/span><span class=\"br0\">(<\/span><span class=\"st0\">\"-e\"<\/span><span class=\"sy0\">,<\/span> <span class=\"st0\">\".\"<\/span><span class=\"br0\">)<\/span><br>\n\u00a0 \u00a0 session.<span class=\"me1\">run<\/span><span class=\"br0\">(<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 <span class=\"st0\">\"pylint\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 <span class=\"st0\">\"--disable=all\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 *<span class=\"br0\">(<\/span>f<span class=\"st0\">\"--enable={checker}\"<\/span> <span class=\"kw1\">for<\/span> checker <span class=\"kw1\">in<\/span> checkers<span class=\"br0\">)<\/span><br>\n\u00a0 \u00a0 \u00a0 \u00a0 <span class=\"st0\">\"src\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 <span class=\"br0\">)<\/span><\/div><\/div><\/pre>\n<h3>3. Checkers<\/h3>\n<p>These are some of the ones I like. Enforce consistency in the project, avoid some obvious mistakes.<\/p>\n<pre>\n<div class=\"geshifilter\"><div class=\"python geshifilter-python\">checkers <span class=\"sy0\">=<\/span> <span class=\"br0\">[<\/span><br>\n\u00a0 \u00a0 <span class=\"st0\">\"missing-class-docstring\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 <span class=\"st0\">\"missing-function-docstring\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 <span class=\"st0\">\"missing-module-docstring\"<\/span><span class=\"sy0\">,<\/span><br>\n\u00a0 \u00a0 <span class=\"st0\">\"function-redefined\"<\/span><span class=\"sy0\">,<\/span><br><span class=\"br0\">]<\/span><\/div><\/div><\/pre>\n<h2>Using PyLint<\/h2>\n<p>You can take just the good parts of PyLint. Run it in CI to keep consistency, and use the highest value checkers.<\/p>\n<p>Lose the bad parts: Default deny checkers.<\/p>\n<p>Avoid the ugly parts: Pin the version to avoid surprises.<\/p>\n<\/div>\n<div class=\"clearfix text-formatted field field--name-field-article-subhead field--type-text-long field--label-hidden field__item\">\n<p>Get the most out of PyLint.<\/p>\n<\/div>\n<div class=\"field field--name-field-lead-image field--type-entity-reference field--label-hidden field__item\">\n<article class=\"media media--type-image media--view-mode-caption\">\n<div class=\"field field--name-field-media-image field--type-image field--label-hidden field__item\">  <img decoding=\"async\" loading=\"lazy\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/09\/python_programming_question.png\" width=\"1041\" height=\"585\" alt=\"Python programming language logo with question marks\" title=\"Python programming language logo with question marks\"><\/div>\n<div class=\"field field--name-field-caption field--type-text-long field--label-hidden caption field__item\"><span class=\"caption__byline\">Image by: <\/span><\/p>\n<p>Opensource.com<\/p>\n<\/div>\n<\/article>\n<\/div>\n<div class=\"field field--name-field-tags field--type-entity-reference field--label-hidden field__items\">\n<div class=\"field__item\"><a href=\"https:\/\/opensource.com\/tags\/python\" hreflang=\"en\">Python<\/a><\/div>\n<\/p><\/div>\n<div class=\"hidden field field--name-field-listicle-title field--type-string field--label-hidden field__item\">What to read next<\/div>\n<div class=\"field field--name-field-default-license field--type-list-string field--label-hidden field__item\"><a rel=\"license\" href=\"http:\/\/creativecommons.org\/licenses\/by-sa\/4.0\/\"><br \/>\n        <img decoding=\"async\" alt=\"Creative Commons License\" src=\"https:\/\/www.cryptocabaret.com\/wp-content\/uploads\/2022\/09\/cc-by-sa--23.png\" title=\"This work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.\"><\/a>This work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.<\/div>\n<section class=\"field field--name-field-comments field--type-comment field--label-hidden comment-wrapper\">\n<div class=\"comments__count\">\n<div class=\"login\"><a href=\"https:\/\/opensource.com\/user\/register?absolute=1\">Register<\/a> or <a href=\"https:\/\/opensource.com\/user\/login?current=\/rss.xml&amp;absolute=1\">Login<\/a> to post a comment.<\/div>\n<\/p><\/div>\n<\/section>\n<p class=\"wpematico_credit\"><small>Powered by <a href=\"http:\/\/www.wpematico.com\" target=\"_blank\" rel=\"noopener\">WPeMatico<\/a><\/small><\/p>\n","protected":false},"excerpt":{"rendered":"<p>PyLint: The good, the bad, and the ugly Moshe Zadka Mon, 09\/19\/2022 &#8211; 03:00 Hot take: PyLint is actually good! &#8220;PyLint can save your life&#8221; is an exaggeration, but not as much as you might think! PyLint can keep you from really really hard to find and complicated bugs. At worst, it can save you [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":67307,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[307],"tags":[],"class_list":["post-67306","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-open-source"],"_links":{"self":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts\/67306","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=67306"}],"version-history":[{"count":0,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/posts\/67306\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=\/wp\/v2\/media\/67307"}],"wp:attachment":[{"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=67306"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=67306"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cryptocabaret.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=67306"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}