Auto import CLI

In response to the recent one-off proposals for adding CLI functionality to modules, another possiblitity would be adding a startup switch to the interpreter that would:

  • Scan the command line for text of the form x.y and automatically try to import “x”
  • Print the return value of the last statement if not None

The switch could be -ai or -am (auto import, auto module). Or (since this is Python), Something Completely DIfferent.

Demo:

python -am 'random.randrange(1,10)'
4
python -am 3 + 8
11
python -am '3 + 8; 2 + 2'
4
python -am 'zipfile.is_zipfile("demo.zip")'
True
python -am zipfile.ZIP_STORED
0
python -am zipfile.ZIP_DEFLATED
8
python -am 'math.sin(3)'
0.1411200080598672
python -am 'datetime.datetime.now()'
2025-03-14 06:33:33.730365

I had ChatGPT gen up a demo implementation:

#!/usr/bin/env python3

import sys
import re
import importlib
import ast

def try_import(module_name):
    """Attempt to import the module and add it to globals() without raising an error."""
    try:
        mod = importlib.import_module(module_name)
        globals()[module_name] = mod
    except ImportError:
        pass  # Ignore import errors

def wrap_last_expr(source_code):
    """
    Parse the source code; if the last statement is an expression,
    replace it with an assignment to __result and an if-statement that
    prints __result if it is not None.
    """
    try:
        tree = ast.parse(source_code, mode="exec")
    except Exception:
        # If parsing fails, compile normally
        return compile(source_code, filename="<string>", mode="exec")
    
    if tree.body and isinstance(tree.body[-1], ast.Expr):
        last_expr = tree.body.pop()
        # Create assignment: __result = (last_expr)
        assign = ast.Assign(
            targets=[ast.Name(id="__result", ctx=ast.Store())],
            value=last_expr.value
        )
        # Create if statement:
        # if __result is not None:
        #     print(__result)
        if_stmt = ast.If(
            test=ast.Compare(
                left=ast.Name(id="__result", ctx=ast.Load()),
                ops=[ast.IsNot()],
                comparators=[ast.Constant(value=None)]
            ),
            body=[ast.Expr(
                value=ast.Call(
                    func=ast.Name(id="print", ctx=ast.Load()),
                    args=[ast.Name(id="__result", ctx=ast.Load())],
                    keywords=[]
                )
            )],
            orelse=[]
        )
        tree.body.append(assign)
        tree.body.append(if_stmt)
        ast.fix_missing_locations(tree)
        return compile(tree, filename="<ast>", mode="exec")
    else:
        return compile(tree, filename="<ast>", mode="exec")

def main():
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} '<python_command>' '<python_command>' ...")
        sys.exit(1)

    # Join all arguments as a single string of Python commands.
    commands = " ".join(sys.argv[1:])

    # Find all occurrences of x.y and attempt to import module x.
    matches = re.findall(r'\b([a-zA-Z_][a-zA-Z0-9_]*)\.', commands)
    for mod in set(matches):
        try_import(mod)

    # Transform the code to capture and print the last expression's result.
    code = wrap_last_expr(commands)
    
    # Execute the compiled code.
    exec(code, globals(), locals())

if __name__ == "__main__":
    main()

1 Like

I’m expressing no opinion. But I’d definitely use this. In particular it would be great to have easier access to pathlib on the command line. However the UI needs tweaking:

In Posix -am gets interpreted as -a -m.

  • Arguments are options if they begin with a hyphen delimiter (‘-’).
  • Multiple options may follow a hyphen delimiter in a single token if the options do not take arguments. Thus, ‘-abc’ is equivalent to ‘-a -b -c’.

But this could work together with -c as -ac (i.e. followed by a -command, that requres -automagickal insertion of imports) just as well as -ic and -im.

Would it lead to endless nuisance support requests when Bash, cmd or Powershell syntax clashes with what some users mistakenly think is their Python code?

3 Likes

I understand that this might be more accessible in the long term as an option to the core interpreter, but honestly, why not just publish that script as a utility? If it’s sufficiently useful to people, making it available now rather than going through the process of a PEP and waiting for a new Python release, seems like it would be better.

1 Like

But only if x isn’t already defined.

I’d rather see a Perl-like solution, with a separate option used to import modules. You still have to be explicit, but you can remove the import noise from your code. Something like

python -M random -c 'random.randrange(1, 10)'

or (to borrow again from what Perl offers)

python -M random=randrange -c 'randrange(1, 10)'

The “assignment” is a little awkward and non-intuitive, but it’s a bit of command-line syntactic sugar which Perl uses to simplify its own form of from random import randrange. The point, though, is not to propose any specific API, only to say that I don’t think inferring imports from your code is a great idea.

2 Likes

If you are looking for one-liners, Python is a wrong tool for this.

2 Likes

I figured I can run the following to print sys.path, given sys is imported in my $PYTHONSTARTUP file:

% python -iq <<<sys.path
>>> ['', '/home/pawamoy/.basher-packages/pyenv/pyenv/versions/3.13.2/lib/python313.zip', '/home/pawamoy/.basher-packages/pyenv/pyenv/versions/3.13.2/lib/python3.13', '/home/pawamoy/.basher-packages/pyenv/pyenv/versions/3.13.2/lib/python3.13/lib-dynload', '/home/pawamoy/.basher-packages/pyenv/pyenv/versions/3.13.2/lib/python3.13/site-packages']
>>>

Works with the zipfile example too :smile:

You can make an alias like alias p='python -iq <<<'.

% p '3 + 2'
>>> 5
>>>

Now we just need to get rid of the prompts! There might be a way thanks to sys.ps1 / sys.ps2.

Too magical for me.

FWIW, this sort of functionality already exists in tools like pyp:

$ pyp 'random.randrange(1, 10)'
4
6 Likes

I think this is fair, you can import multiple modules with -M, would be slightly less noisy than doing it all in a -c string

My motivation is not that I want to or would use Python as a command line tool. The only modules I generally use are -m pip, -m venv, -m ensure-pip.

(I work in a 99.5% *nix environment and use GNU utliities, bash, and bash scripts for simple tasks. I have a startpy bash script that spits out an argparse template with a logger. I use that for Python one-trick pony scripts where I want to type Python’s richer than bash environment.)

We just seem to get repeated ideas / requests for command line functionality and we’ve limited support for it. For example, random added some switches in 3.13 following last year’s discussion., but you can do things like
random.randrange(5,10)

It just seems incoherent.

3 Likes