Home > python > ChDir: a context manager for switching working directories

ChDir: a context manager for switching working directories

Problem
In your program you want to change the working directory temporarily, do some job there, then switch back to the original directory. Say you want to download some images to /tmp. When done, you want to get back to the original location correctly, even if an exception was raised at the temp. location.

Naïve way
Let’s see the following example. We have a script, say at /home/jabba/python/fetcher.py . We want to download some images to /tmp, then work with them. After the download we want to create a subfolder “process” in the same directory where the script fetcher.py is located. We want to collect some extra info about the downloaded images and we want to store these pieces of information in the “process” folder.

import os

def download(li, folder):
    try:
        backup = os.getcwd()
        os.chdir(folder)
        for img in li:
            # download img somehow
        os.chdir(backup)
    except:
        # problem with download, handle it

def main():
    # step 1: download images to /tmp
    li = ["http://...1.jpg", "http://...2.jpg", "http://...3.jpg"]
    download(li, "/tmp")
    # step 2: create a "process" dir. HERE (where the script was launched)
    os.mkdir("process")
    # ...do some extra work...

There is a problem with the download method. If an image cannot be downloaded correctly and an exception occurs, we return from the method. However, os.chdir(backup) is not executed and we remain in the /tmp folder! In main() in step 2 the process directory will be created in /tmp and not in the folder where we wanted it to be.

Well, you can always add a finally block to the exception handler and place os.chdir(backup) there, but it’s easy to forget. Is there an easier solution?

Solution
Yes, there is an easier solution. Use a context manager.

The previous example with a context manager:

import os

def download(li, folder):
    with ChDir(folder):
        for img in li:
            # download img somehow

def main():
    # step 1: download images to /tmp
    li = ["http://...1.jpg", "http://...2.jpg", "http://...3.jpg"]
    download(li, "/tmp")
    # step 2: create a "process" dir. HERE (where the script was launched)
    os.mkdir("process")
    # ...do some extra work...

And now the source code of ChDir:

import os

class ChDir(object):
    """
    Step into a directory temporarily.
    """
    def __init__(self, path):
        self.old_dir = os.getcwd()
        self.new_dir = path

    def __enter__(self):
        os.chdir(self.new_dir)

    def __exit__(self, *args):
        os.chdir(self.old_dir)

Since ChDir is a context manager, you use it in a with block. At the beginning of the block you enter the given folder. When you leave the with block (even if you leave because of an exception), you are put back to the folder where you were before entering the with block.

Update
Following this discussion thread @reddit, someone suggested using the PyFilesytem library. I think PyFilesytem is a very good solution but it may be too much for a short script. It’s like shooting a sparrow with a cannon :) For a simple script ChDir is good enough for me. For a serious application, check out PyFilesytem.

Advertisements
  1. devilirium
    December 22, 2013 at 19:30

    Actually, it’s a really interesting post, since if you think about it, everything we do has a create() -> use() -> destroy() cycle! So magic functions on classes all the way.

  2. Vasily
    April 3, 2018 at 15:04

    Even shorter solution with python’s contextlib library (available from python 2.5):

    @contextmanager
    def chdir(target_dir):
        original_dir = os.getcwd()
        os.chdir(target_dir)
        yield
        os.chdir(original_dir)
    
  3. May 14, 2018 at 21:08

    No, please, GOD, NOOOOOOOOO. Don’t teach things like this to unsuspecting people.

    This does NOT work for anything that reeks of global/shared state. As soon as you use Threads or Async and two or more of those “with MutateMyGlobalState(): do_wtf()”, weird things are gonna happen.

    Weird as in the last assignments are gonna win. Both for __enter__, and then for __exit__. So the files will be in random directories, and unless the first context manager stops as last, you’re gonna end in another dir then you started in.

    Beware, girls and boys, the sneaky, bloodthirsty monster of shared, mutable state. It lurks in places like “current directory”, “environment variables”, “current locale”, “date/time format”, etc. If need be, make it an instance variable / pass it into function calls.

  1. No trackbacks yet.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

%d bloggers like this: