Package: perflint
perflintA Linter for performance anti-patternsThis project is an early beta. It will likely raise many false-positives in your code.Installationpip install perflintUsagePerflint can be used as a standalone linter:perflint your_code/Or as apylintlinter plugin:pylint your_code/ --load-plugins=perflintVS CodeAdd these configuration properties to your.vscode/settings.jsonfile (create if it doesn't exist):{"python.linting.pylintEnabled":true,"python.linting.enabled":true,"python.linting.pylintArgs":["--load-plugins","perflint","--rcfile","${workspaceFolder}/.pylintrc"],}RulesW8101 : Unnecessarylist()on already iterable type (unnecessary-list-cast)Using alist()call to eagerly iterate over an already iterable type is inefficient as a second list iterator is created, after first iterating the value:defsimple_static_tuple():"""Test warning for casting a tuple to a list."""items=(1,2,3)foriinlist(items):# [unnecessary-list-cast]print(i)W8102: Incorrect iterator method for dictionary (incorrect-dictionary-iterator)Python dictionaries store keys and values in two separate tables. They can be individually iterated. Using.items()and discarding either the key or the value using_is inefficient, when.keys()or.values()can be used instead:defsimple_dict_keys():"""Check that dictionary .items() is being used correctly. """fruit={'a':'Apple','b':'Banana',}for_,valueinfruit.items():# [incorrect-dictionary-iterator]print(value)forkey,_infruit.items():# [incorrect-dictionary-iterator]print(key)W8201: Loop invariant statement (loop-invariant-statement)The body of loops will be inspected to determine statements, or expressions where the result is constant (invariant) for each iteration of a loop. This is based on named variables which are not modified during each iteration.For example:defloop_invariant_statement():"""Catch basic loop-invariant function call."""x=(1,2,3,4)foriinrange(10_000):# x is never changed in this loop scope,# so this expression should be evaluated outsideprint(len(x)*i)# [loop-invariant-statement]#     ^^^^^^len(x)should be evaluated outside the loop sincexis not modified within the loop.defloop_invariant_statement():"""Catch basic loop-invariant function call."""x=(1,2,3,4)n=len(x)foriinrange(10_000):print(n*i)# [loop-invariant-statement]The loop-invariance checker will underline expressions and sub-expressions within the body using the same rules:defloop_invariant_statement_more_complex():"""Catch basic loop-invariant function call."""x=[1,2,3,4]i=6forjinrange(10_000):# x is never changed in this loop scope,# so this expression should be evaluated outsideprint(len(x)*i+j)#             ^^^^^^^^^^    [loop-invariant-statement]Methods are blindly considered side-effects, so if a method is called on a variable, it is assumed to have possibly changed in value and therefore not loop-invariant:defloop_invariant_statement_method_side_effect():"""Catch basic loop-invariant function call."""x=[1,2,3,4]i=6forjinrange(10_000):print(len(x)*i+j)x.clear()# x changes as a side-effectThe loop-invariant analysis will walk up the AST until it gets to the whole loop body, so an entire branch could be marked.
For example, the expressionlen(x) > 2is invariant and therefore should be outside the loop. Also, becausex * iis invariant, that statement should also be outside the loop, therefore the entire branch will be marked:defloop_invariant_branching():"""Ensure node is walked up to find a loop-invariant branch"""x=[1,2,3,4]i=6forjinrange(10_000):# Marks entire branchiflen(x)>2:print(x*i)Notes on loop invarianceFunctions can have side-effects (print is a good example), so the loop-invariant scanner may give some false-positives.It will also highlight dotted expressions, e.g. attribute lookups. This may seem noisy, but in some cases this is valid, e.g.fromos.pathimportexistsimportosdefdotted_import():for_inrange(100_000):returnos.path.exists('/')defdirect_import():for_inrange(100_000):returnexists('/')direct_import()is 10-15% faster thandotted_import()because it doesn't need to load theosglobal, thepathattribute and theexistsmethod for each iteration.W8202: Global name usage in a loop (loop-global-usage)Loading globals is slower than loading "fast" local variables. The difference is marginal, but when propagated in a loop, there can be a noticeable speed improvement, e.g.:d={"x":1234,"y":5678,}defdont_copy_dict_key_to_fast():for_inrange(100000):d["x"]+d["y"]d["x"]+d["y"]d["x"]+d["y"]d["x"]+d["y"]d["x"]+d["y"]defcopy_dict_key_to_fast():i=d["x"]j=d["y"]for_inrange(100000):i+ji+ji+ji+ji+jcopy_dict_key_to_fast()executes 65% faster thandont_copy_dict_key_to_fast()R8203 : Try..except blocks have a significant overhead. Avoid using them inside a loop (loop-try-except-usage).Up to Python 3.10,try...exceptblocks are computationally expensive compared withifstatements.Avoid using them in a loop as they can cause significant overheads. Refactor your code to not require iteration specific details and put the entire loop in the body of atryblock.W8204 : Looped slicing of bytes objects is inefficient. Use a memoryview() instead (memoryview-over-bytes)Slicing ofbytesis slow as it creates a copy of the data within the requested window. Python has a builtin type,memoryviewforzero-copy interactions:defbytes_slice():"""Slice using normal bytes"""word=b'A'*1000foriinrange(1000):n=word[0:i]#   ^^^^^^^^^ memoryview-over-bytesdefmemoryview_slice():"""Convert to a memoryview first."""word=memoryview(b'A'*1000)foriinrange(1000):n=word[0:i]memoryview_slice()is 30-40% faster thanbytes_slice()W8205 : Importing the "%s" name directly is more efficient in this loop. (dotted-import-in-loop)In Python you can import a module and then access submodules as attributes. You can also access functions as attributes of that module. This keeps your import statements minimal, however, if you use this method in a loop it is inefficient because each loop iteration it will load global, load attribute and then load method. Because the name isn't an object, "load method" falls back to load attribute via a slow internal path.Importing the desired function directly is 10-15% faster:importos# NOQAdeftest_dotted_import(items):foriteminitems:val=os.environ[item]# Use `from os import environ`defeven_worse_dotted_import(items):foriteminitems:val=os.path.exists(item)# Use `from os.path import exists` insteadW8301 : Use tuple instead of list for a non-mutated sequence. (use-tuple-over-list)Constructing a tuple is faster than a list and indexing tuples is faster. When the sequence is not mutated, then a tuple should be used instead:defindex_mutated_list():fruit=["banana","pear","orange"]fruit[2]="mandarin"len(fruit)foriinfruit:print(i)defindex_non_mutated_list():fruit=["banana","pear","orange"]# Raises [use-tuple-over-list]print(fruit[2])len(fruit)foriinfruit:print(i)Mutation is determined by subscript assignment, slice assignment, or methods called on the list.W8401 : Use a list comprehension instead of a for-loop (use-list-comprehension)List comprehensions are 25% more efficient at creating new lists, with or without an if-statement:defshould_be_a_list_comprehension_filtered():"""A List comprehension would be more efficient."""original=range(10_000)filtered=[]foriinoriginal:ifi%2:filtered.append(i)W8402 : Use a list copy instead of a for-loop (use-list-copy)Use either thelist()constructor orlist.copy()to copy a list, not another for loop:defshould_be_a_list_copy():"""Using the copy() method would be more efficient."""original=range(10_000)filtered=[]foriinoriginal:filtered.append(i)W8403 : Use a dictionary comprehension instead of a for-loop (use-dict-comprehension)Dictionary comprehensions should be used in simple loops to construct dictionaries:defshould_be_a_dict_comprehension():pairs=(("a",1),("b",2))result={}forx,yinpairs:result[x]=ydefshould_be_a_dict_comprehension_filtered():pairs=(("a",1),("b",2))result={}forx,yinpairs:ify%2:result[x]=y