Skip to main content

Static code analyser for Python3 function calls, string interpolation and variable shadowing

Project description

pystrict3

pystrict3 checks Python3 code for simple mistakes, such as

  • calling functions with the wrong number of arguments,

  • interpolating strings with the wrong number of arguments,

  • shadowing and re-using variables

This complements other static analysers such as pyflakes, and can be used alongside linters.

Rules

pystrict3 enforces that variables are only assigned once, and that python keywords are not overwritten. This avoids shadowing and change of semantics of variables, and leads to cleaner, more idiomatic code:

parse = parse(foo)  ## bad
node = get_node()
node.foo()  ## ok, modification
node += 3   ## ok, modification

def format(...): ## bad, format is a keyword

import requests, html

html = requests.get(url)  ## bad: overwrites imported package name

pystrict3 checks that functions are called with the right number of arguments. This catches bugs before execution, for example when call signatures change to include an additional argument:

def foo(a, b):
    return a*b
foo(1, 2)  ## OK
foo(123)  ## error: wrong number of arguments

def bar(a, b=1):
    return a*b
bar(1)  ## OK
bar(1, 2)  ## OK
bar(1, 2, 3)  ## error: wrong number of arguments

pystrict3 checks that printf-style string interpolation is used with the right number of arguments:

print("Hello %s, it is %d:%02d" % ("World", 12, 34)) # OK
print("Hello %s, it is %d:%02d" % ("World", 12)) # error
'\n%s%*s' % (indent, len(key)+3, '')  # ok, variable length
'{}{}'.format(1,2) # ok
'{}{}'.format(1) # error
'{}{}'.format(1,2,3) # error
'{0}{1}{0}{2}'.format(1,2)  # error: missing index 2
'{foo}{bar}'.format(foo=1, bar=2, baz=3)  # ok
'{foo}{bar}'.format(foo=1, baz=3)  # error: missing bar

Contributing

Contributions are welcome.

pystrict3 is currently relatively dumb and may not catch all corner cases. It tries hard to avoid unintentional false positives.

However, it is tested on activestate recipes, and 1131/1256 of all valid python3 programs already are pystrict3 compliant, indicating that its guidelines are already adhered to.

Usage

Run with:

$ python3 pystrict3.py <filenames>

Running with multiple filenames has the benefit that all function signatures are first recorded and verified across all files.

Example stderr outputs:

tests/expect-fail/recipe-412717.py:32: ERROR: Variable reuse: "Test"
tests/expect-fail/recipe-425043.py:13: ERROR: Function "pow" (3..3 arguments) called with 2 arguments
tests/expect-fail/recipe-578135.py:184: ERROR: Function "encode" (3..3 arguments) called with 2 arguments

Return code is 1 if a error was detected, or 0 otherwise. For non-verbose, pipe stdout to /dev/null.

Future

Programs where variables do not change are easier to optimize and parallelise. Perhaps in the future, python compilers can take advantage of this.

Licence

BSD 2-clause.

Tipps

It’s OK to not be pystrict3 compliant. It can serve as guidance towards cleaner code.

How to write to pystrict3 compliance:

  • Use del to actively remove unused variables:

    answer = input("Do you want to play? (yes/no)")
    if answer == "no":
        sys.exit()
    del answer
    answer = int(input("first value"))
    print(answer * 10):
  • Name parts of computation explicitly:

    filename = 'foo.pdf'
    if condition:
       filename = 'foo.png'  # bad
    
    # better:
    if condition:
       filename = 'foo.png'
    else:
       filename = 'foo.pdf'
    
    # bad:
    path = os.path.basename(sys.argv[1])
    path = path + filename   # bad: variable changes meaning
    path = path + extension
    
    # better:
    components = []
    components.append(os.path.basename(sys.argv[1]))
    components.append(filename)
    components.append(extension)
    path = ''.join(components)
  • Refactor into functions:

    # original: "changes" is being reused.
    USE_JYTHON = False
    try:
        # ... code detecting something, which throws an exception
        USE_JYTHON = True
    except:
        pass
    
    # new: make a function for detection:
    try:
        # ... code detecting something, which throws an exception
        USE_JYTHON = True
    except:
        USE_JYTHON = False
    
    # original: a sorting construct
    
    changes = True
    while changes:
        changes = False
        for a in ...:
            if ...:
                changes = True
                break
        if not changes:
            break
    
    # new: function returns when no further changes are needed
    def mysort(objs):
        while True:
            changes = False
            for a in ...:
                if ...:
                    changes = True
                    break
            if not changes:
                return objs
  • Instead of assigning to __doc__, move the docstring to the start of the file.

Release Notes

0.1.0 (2020-03-07)

  • First version

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pystrict3-0.2.0.tar.gz (7.3 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page