Running Code Files in IPython — Learn IPython Day 5

Running Code Files in IPython — Learn IPython Day 5

IPython supports several ways to interactively run Python code. In today's lesson, you'll learn how to execute scripts from within the IPython REPL using the %run magic function. You'll also learn how to pass arguments to scripts that you execute with %run and how %run handles namespaces.

Learning Outcomes

By the end of today, you'll know:

  • What the %run magic command is and how to use it
  • How to share your IPython namespace with a script using the -i flag
  • How namespace conflicts are resolved when using %run

Pre-requisites

Before you start today's lessons, make sure you have a working Python installation on your computer (preferably Python 3.8 or greater). You should also have IPython installed and have worked through the Day 4 lesson:

IPython Magic Functions — Learn IPython Day 4
IPython’s magic function system exposes a rich set of commands that can be used to configure IPython, run and edit code, and inspect code objects.

Running Files With %run

The %run magic is used to run a Python file from within the IPython REPL. This is useful for loading code into an interactive session and then using objects from the file interactively.

For instance, say you have a file greet.py with a function greet() defined inside of it:

# greet.py

def greet(name):
    print(f"Hello, {name}")

Open a new IPython session and run %whos to verify that you have an empty namespace:

In [1]: %whos
Interactive namespace is empty.

Now type %run greet.py to execute greet.py and load the greet() function into your IPython namespace:

In [2]: %run greet.py

In [3]: %whos
Variable   Type        Data/Info
--------------------------------
greet      function    <function greet at 0x103346d40>

Now that greet() is in your namespace, you can interact with it:

In [4]: greet("David")
Hello, David

So, why load a file this way instead of just importing it and using the contents? %run a great way to test executing a script while also getting all of the objects defined within it into your namespace. You'll be able to test execution and also inspect the state of the program when it is done running.

It's true that when you import a module in IPython, or in any Python program for that matter, the module gets executed. However, any code protected by an if __name__ == "__main__" clause won't get executed during import but will get executed with %run. Using %run is roughly equivalent to executing a Python script using the python -i command.

To see this in action, change the greet.py file to include a protected call to greet():

# greet.py

def greet(name):
    print(f"Hello, {name}")


if __name__ == "__main__":
    greet("David")

Now when you %run the file, you'll see "Hello, David" printed to the console:

In [5]: %run greet.py
Hello, David

By default, the %run magic runs Python scripts in an empty namespace, so even if you already have a greet() so referencing names defined in your IPython namespace in the script will result in a NameError.

For example, suppose you change the greet.py script to call greet() with an argument called my_name:

# greet.py

def greet(name):
    print(f"Hello, {name}")


if __name__ == "__main__":
    greet(my_name)

When you %run greet.py, you'll get a NameError:

In [6]: %run greet.py
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
File ~/greet.py:6, in <module>
      2     print(f"Hello, {name}")
      5 if __name__ == "__main__":
----> 6     greet(my_name)

NameError: name 'my_name' is not defined

However, creating a variable called my_name in IPython and then running the script using the -i flag will execute the file using the IPython namespace:

In [7]: my_name = "David"

In [8]: %run -i greet.py
Hello, David

Sharing your IPython namespace with a script might seem like a strange thing to do, but it opens up a powerful way to build up a script from within an interactive session.

You can start by experimenting with code in IPython and then incrementally move snippets of code to your script as they become more solidified. When you are done, you can run the script without the -i flag to check that everything works from a blank namespace.

Passing Arguments To a Script With %run

When you write scripts that automate a process, it's common to accept command-line arguments. You can pass these arguments to the script when you execute it with %run similar to how you'd pass arguments to the script from the command line using the python command.

For instance, suppose the greet.py scripts uses sys.argv to accept an argument for the name to greet:

# greet.py

def greet(name):
    print(f"Hello, {name}")


if __name__ == "__main__":
    import sys
    args = sys.argv
    name = args[1]  # args[0] is the filename!
    greet(name)

To pass an argument to greet.py with  %run, all you need to do is include it after the script name:

In [9]: %run greet.py David
Hello, David

Since the namespace used to run the file gets dumped into the IPython namespace after execution, you can even see the value of the args and name variables in the script from within IPython after running the program:

In [10]: whos
Variable   Type        Data/Info
--------------------------------
args       list        n=2
greet      function    <function greet at 0x1080f3d00>
my_name    str         David
name       str         David
sys        module      <module 'sys' (built-in)>

Notice that the sys module is also available to you, so you can now use sys in IPython as if you'd explicitly imported it:

In [11]: sys.platform
Out[11]: 'darwin'

In [12]: sys.version
Out[12]: '3.10.2 (main, Feb  2 2022, 05:51:25) [Clang 13.0.0 (clang-1300.0.29.3)]'

You can pass arguments to any Python script that accepts them, even if they are parsed by argparse instead of being captured by sys.argv. For instance, rewrite the greet.py script to use argparse, as follows:

# greet.py

def greet(name):
    print(f"Hello, {name}")


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Greet someone by name")
    parser.add_argument("name", type=str, help="Name of the person to greet")
    args = parser.parse_args()
    greet(args.name)

Passing the argument to greet.py from %run works as expected:

In [13]: %run greet.py David
Hello, David

You can even pass arguments like --help to the script to see the help page generated by argparse:

In [14]: %run greet.py --help
usage: greet.py [-h] name

Greet someone by name

positional arguments:
  name        Name of the person to greet

options:
  -h, --help  show this help message and exit

Namespaces And The %run Command

By default, scripts executed with %run are executed inside a blank namespace, just like running a script with the python command from the command line. As you saw previously, you can share your current IPython namespace with a script by running it with %run -i.

In both cases, the IPython namespace gets updated with the state of the script's namespace when it is done executing. But what if there are namespace conflicts? There are two situations where conflicts could occur:

  1. You execute a script with %run and the script assigns a value to a name that currently exists in your IPython namespace.
  2. You execute a script with %run -i and the script overwrites a name that is currently in your namespace.

In the first case, there is no conflict to deal with when the script is running, since %run executes the script in a blank namespace. The issue is what happens when the script finishes running and the script's namespace gets dumped into the IPython namespace.

In both cases, though, the question is: what is the value of the original name in the IPython namespace after the script runs?

Let's go back to the greet.py example and see what happens. Re-write greet.py to declare a my_name variable that gets passed to greet():

# greet.py

def greet(name):
    print(f"Hello, {name}")


if __name__ == "__main__":
    my_name = "David"
    greet(my_name)

Now, restart your IPython session so that you have a blank IPython namespace. Confirm this with the %whos command:

In [1]: %whos
Interactive namespace is empty.

Now create a variable called my_name and assign to it the string "Amos". Then run the script and inspect the IPython namespace again to see how it's changed:

In [2]: my_name = "Amos"

In [3]: %run greet.py
Hello, David

In [4]: %whos
Variable   Type        Data/Info
--------------------------------
greet      function    <function greet at 0x10733e830>
my_name    str         David

my_name has now taken on the value that it was set to inside of greet.py. If you're familiar with how namespace updates work in Python, then this result may not surprise you. However, it's important to keep this in mind when working with files interactively.

Calling %run with -i does not change this behavior. For instance, setting my_name back to "Amos" and running greet.py with %run -i results in the same namespace after the script executes:

In [5]: my_name = "Amos"

In [6]: %run -i greet.py
Hello, David

In [7]: %whos
Variable   Type        Data/Info
--------------------------------
greet      function    <function greet at 0x10733fe20>
my_name    str         David

The key takeaway is: neither Python nor IPython notifies you of namespace conflicts. A name always takes on the last value assigned to it, and in the case of a conflict — such as when the contents of a script's namespace are dumped into the IPython namespace — the value from the script takes precedence.

Day 5 Activity

Continue to use IPython as your Python REPL. Rather than closing IPython or opening a new terminal tab or window to run scripts, practice running them from within IPython and inspecting the namespace once the script executes.

Take some notes while you do this:

  • How does running scripts with %run affect your productivity? Do you feel more or less productive?
  • What advantages have you found by running scripts with %run?
  • What pain points have you encountered while using %run? Does switching between IPython and your editor to edit scripts between runs slow you down at all?

Thanks for sticking around for Day 5 of Learn IPython in 10 days. See you tomorrow for Day 6!


Enjoying this course? Why not share it with a friend or colleague that you think will benefit from it? Just right-click on this link, copy the URL, and send it to them in an email!