182

I need to be able to open a document using its default application in Windows and Mac OS. Basically, I want to do the same thing that happens when you double-click on the document icon in Explorer or Finder. What is the best way to do this in Python?

smci
  • 32,567
  • 20
  • 113
  • 146
Abdullah Jibaly
  • 53,220
  • 42
  • 124
  • 197
  • 12
    There's been an issue for this to be included in the standard library in the Python tracker from 2008: http://bugs.python.org/issue3177 – Ram Rachum Jul 09 '11 at 12:17

17 Answers17

216

Use the subprocess module available on Python 2.4+, not os.system(), so you don't have to deal with shell escaping.

import subprocess, os, platform
if platform.system() == 'Darwin':       # macOS
    subprocess.call(('open', filepath))
elif platform.system() == 'Windows':    # Windows
    os.startfile(filepath)
else:                                   # linux variants
    subprocess.call(('xdg-open', filepath))

The double parentheses are because subprocess.call() wants a sequence as its first argument, so we're using a tuple here. On Linux systems with Gnome there is also a gnome-open command that does the same thing, but xdg-open is the Free Desktop Foundation standard and works across Linux desktop environments.

fearless_fool
  • 33,645
  • 23
  • 135
  • 217
Nick
  • 21,555
  • 18
  • 47
  • 50
  • 6
    Using 'start' in subprocess.call() doesn't work on Windows -- start is not really an executable. – Tomas Sedovic Oct 18 '09 at 19:10
  • 4
    nitpick: on all linuxen (and I guess most BSDs) you should use `xdg-open` - http://linux.die.net/man/1/xdg-open – gnud Oct 18 '09 at 19:57
  • 8
    start on Windows is a shell command, not an executable. You can use subprocess.call(('start', filepath), shell=True), although if you're executing in a shell you might as well use os.system. – Peter Graham Feb 03 '11 at 23:25
  • I ran `xdg-open test.py` and it opened firefox download dialog for me. What's wrong? I'm on manjaro linux. – Jason Sep 12 '18 at 03:28
  • 1
    @Jason Sounds like your `xdg-open` configuration is confused, but that's not really something we can troubleshoot in a comment. Maybe see https://unix.stackexchange.com/questions/36380/how-to-properly-and-easy-configure-xdg-open-without-any-environment – tripleee May 08 '19 at 06:20
  • Nitpick: `subprocess.call()` runs the command, but doesn't check its exit code. You should probably prefer `subprocess.check_call()` or its modern-day equivalent `subprocess.run(..., check=True)` – tripleee May 08 '19 at 06:22
  • @tripleee thanks for the info. Mine was in `openbox` too. I've moved to cinnamon and `xdg-open` works correctly now. – Jason May 08 '19 at 13:14
101

open and start are command-interpreter things for Mac OS/X and Windows respectively, to do this.

To call them from Python, you can either use subprocess module or os.system().

Here are considerations on which package to use:

  1. You can call them via os.system, which works, but...

    Escaping: os.system only works with filenames that don't have any spaces or other shell metacharacters in the pathname (e.g. A:\abc\def\a.txt), or else these need to be escaped. There is shlex.quote for Unix-like systems, but nothing really standard for Windows. Maybe see also python, windows : parsing command lines with shlex

    • MacOS/X: os.system("open " + shlex.quote(filename))
    • Windows: os.system("start " + filename) where properly speaking filename should be escaped, too.
  2. You can also call them via subprocess module, but...

    For Python 2.7 and newer, simply use

    subprocess.check_call(['open', filename])
    

    In Python 3.5+ you can equivalently use the slightly more complex but also somewhat more versatile

    subprocess.run(['open', filename], check=True)
    

    If you need to be compatible all the way back to Python 2.4, you can use subprocess.call() and implement your own error checking:

    try:
        retcode = subprocess.call("open " + filename, shell=True)
        if retcode < 0:
            print >>sys.stderr, "Child was terminated by signal", -retcode
        else:
            print >>sys.stderr, "Child returned", retcode
    except OSError, e:
        print >>sys.stderr, "Execution failed:", e
    

    Now, what are the advantages of using subprocess?

    • Security: In theory, this is more secure, but in fact we're needing to execute a command line one way or the other; in either environment, we need the environment and services to interpret, get paths, and so forth. In neither case are we executing arbitrary text, so it doesn't have an inherent "but you can type 'filename ; rm -rf /'" problem, and if the file name can be corrupted, using subprocess.call gives us little additional protection.
    • Error handling: It doesn't actually give us any more error detection, we're still depending on the retcode in either case; but the behavior to explicitly raise an exception in the case of an error will certainly help you notice if there is a failure (though in some scenarios, a traceback might not at all be more helpful than simply ignoring the error).
    • Spawns a (non-blocking) subprocess: We don't need to wait for the child process, since we're by problem statement starting a separate process.

    To the objection "But subprocess is preferred." However, os.system() is not deprecated, and it's in some sense the simplest tool for this particular job. Conclusion: using os.system() is therefore also a correct answer.

    A marked disadvantage is that the Windows start command requires you to pass in shell=True which negates most of the benefits of using subprocess.

tripleee
  • 175,061
  • 34
  • 275
  • 318
Charlie Martin
  • 110,348
  • 25
  • 193
  • 263
  • 3
    Depending where `filename` comes form, this is a perfect example of why os.system() is insecure and bad. subprocess is better. – Devin Jeanpierre Mar 28 '09 at 16:36
  • 1
    not to mention the wonders that could be done by changing the default binding for the filetype. Get a grip: this was to show in a simple example how open and start would be used. details of Popen.subprocess would just be in the way. – Charlie Martin Mar 28 '09 at 18:57
  • 7
    Nick's answer looked fine to me. Nothing got in the way. Explaining things using wrong examples isn't easily justifiable. – Devin Jeanpierre Mar 29 '09 at 17:10
  • 2
    It's less secure and less flexible than using subprocess. That sounds wrong to me. – Devin Jeanpierre Mar 29 '09 at 18:35
  • 2
    Security issues of this sort affect not only the original programmer, not just the people who use the program, but potentially everybody on the internet. You need a damn good reason to justify such an educational shortcut, and since this one is trivial to do right... there's no excuse. – Rhamphoryncus Mar 30 '09 at 05:38
  • 1
    and it still doesn't matter a bit to answering the original question. Nor, since open/start have MASSIVE security issues of their own, does it make a lot of difference. If you want to deprecate os.system, take it to Guido. – Charlie Martin Mar 30 '09 at 17:38
  • 8
    Of course it matters. It's the difference between a good answer and a bad answer (or a terrible answer). The docs for os.system() themselves say "Use the subprocess module." What more is needed? That's deprecation enough for me. – Devin Jeanpierre Mar 30 '09 at 19:46
  • os.system() is a mapping of the C API. There's a lot of existing code built on it (not all of which is vulnerable.) Marking it deprecated wouldn't change anything, as you'd simply ignore what you're told and continue advocating it. – Rhamphoryncus Mar 31 '09 at 02:35
  • 1
    OMG, it's a mapping of the C library. That tears it. – Charlie Martin Mar 31 '09 at 02:45
  • 20
    I feel a bit reluctant to restart this discussion, but I think the "Later update" section gets it entirely wrong. The problem with `os.system()` is that it uses the shell (and you are not doing any shell escaping here, so Bad Things will happen for perfectly valid filenames that happen to contain shell meta-characters). The reason why `subprocess.call()` is preferred is that you have the option to bypass the shell by using `subprocess.call(["open", filename])`. This works for all valid filenames, and doesn't introduce a shell-injection vulnerability even for untrusted filenames. – Sven Marnach Apr 23 '12 at 14:55
  • Sven, particularly in the case of `start`, that's a built-in in CMD.exe. How do you propose to avoid the shell? You might have a point with `open` since I beelive it more or less directly goes to `launchd`. – Charlie Martin Nov 04 '12 at 01:07
  • I like the ´os.system("start "+filename)´ solution since I'm only going to use it on windows. But how do you close that process? – Norfeldt Jul 16 '13 at 07:41
  • Once you do start, the new process is separate; it runs to completion however it completes. Your call to `os.system()` returns (almost) immediately. If you need to control the subprocess, you should use os.subprocess. – Charlie Martin Jul 17 '13 at 13:08
  • 2
    I had problems with weird characters in filenames (namely ![], and space), so I had to do `subprocess.check_call(('start', '', filename), shell=True)` The first argument to start is apparently the title. (check_call will raise an error if return code is not 0, I prefer it in most situations to `subprocess.call`) (nm apparently `os.startfile` is a thing) – Mark Harviston May 09 '14 at 07:55
  • Quick thing to mention here: the `subprocess` is only for python 2.7. In Python 3, you would have to change the file with the chevrons to the `file` argument in your `print` function. – Zizouz212 Jun 01 '15 at 14:44
  • 2
    It might be worth adding that on Windows, if the filename contains spaces, you can still use `os.system`: `os.system('start "" "' + filename + '"')` (The `""` is necessary so that Windows does not interpret the file name as the title of the new window.) – schtandard Feb 07 '17 at 11:53
  • Adding quotes only works if you know that `filename` doesn't itself contain quotes or other metacharacters. The proper solution is to use a dedicated quoting function; but using `subprocess` without `shell=True` lets you completely bypass this complication. – tripleee May 08 '19 at 06:13
  • @Zizouz212 `subprocess` was introduced in Python 2.4, though e.g. the `check_call` function was not included in the original release. If you have to be compatible that far, use `subprocess.call()` and live with its flaws, or reimplement `check_call` using bare-bones `p = subprocess.Popen(...) ; result = p.wait(); if result != 0: raise HorriblePanic()` – tripleee May 08 '19 at 06:24
  • 1
    @MarkHarviston Even if your "workaround" worked for you, it still has inherent bugs. You want to avoid `shell=True` entirely, otherwise you're back in the same can of worms that infests `os.system()`. See also https://stackoverflow.com/questions/3172470/actual-meaning-of-shell-true-in-subprocess – tripleee May 08 '19 at 06:30
  • Boo, hiss for showing the Python 2.4 example without escaping. – Charles Duffy Dec 31 '22 at 00:39
  • @CharlesDuffy look at the date on the answer. – Charlie Martin Jan 06 '23 at 02:41
  • @CharlieMartin It's not the "Python 2.4" that I was complaining about (that part was for identification purposes) but the "without escaping". That stands: we shouldn't be showing practices with shell injection vulnerabilities. – Charles Duffy Jan 06 '23 at 11:06
61

I prefer:

os.startfile(path, 'open')

Note that this module supports filenames that have spaces in their folders and files e.g.

A:\abc\folder with spaces\file with-spaces.txt

(python docs) 'open' does not have to be added (it is the default). The docs specifically mention that this is like double-clicking on a file's icon in Windows Explorer.

This solution is windows only.

angussidney
  • 686
  • 2
  • 13
  • 24
DrBloodmoney
  • 2,786
  • 2
  • 18
  • 17
  • 1
    Thanks. I didn't notice the availability, since the docs have it appended to the last paragraph. In most other sections, the availability note occupies its own line. – DrBloodmoney Jan 12 '09 at 17:14
  • 1
    On Linux for some reason, rather than raising an error, the `startfile` function doesn't even exist, which means that users will get a confusing error message about a missing function. You might want to check the platform to avoid this. – c z Feb 14 '20 at 10:20
  • os.startfile supports pathlib like objects whereas other filename based solutions do not – Phillmac Oct 23 '20 at 10:10
42

Just for completeness (it wasn't in the question), xdg-open will do the same on Linux.

Dalton
  • 347
  • 3
  • 15
dF.
  • 74,139
  • 30
  • 130
  • 136
26
import os
import subprocess

def click_on_file(filename):
    '''Open document with default application in Python.'''
    try:
        os.startfile(filename)
    except AttributeError:
        subprocess.call(['open', filename])
Nicu Tofan
  • 1,052
  • 14
  • 34
nosklo
  • 217,122
  • 57
  • 293
  • 297
  • 2
    Huh, I didn't know about startfile. It would be nice if the Mac and Linux versions of Python picked up similar semantics. – Nick Jan 12 '09 at 18:27
  • 3
    Relevant python bug: http://bugs.python.org/issue3177 - provide a nice patch, and it might get accepted =) – gnud Oct 18 '09 at 20:01
23

If you have to use an heuristic method, you may consider webbrowser.
It's standard library and despite of its name it would also try to open files:

Note that on some platforms, trying to open a filename using this function, may work and start the operating system’s associated program. However, this is neither supported nor portable. (Reference)

I tried this code and it worked fine in Windows 7 and Ubuntu Natty:

import webbrowser
webbrowser.open("path_to_file")

This code also works fine in Windows XP Professional, using Internet Explorer 8.

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
etuardu
  • 5,066
  • 3
  • 46
  • 58
  • 3
    As far as I can tell, this is by far the best answer. Seems cross-platform and no need to check which platform is in use or import os, platform. – polandeer May 08 '13 at 23:37
  • 2
    @jonathanrocher: I see [Mac support in the source code](https://github.com/python/cpython/blob/78d05eb847c6b8fede08ca74bb59210c00e4c599/Lib/webbrowser.py#L626). It uses `open location` there that should work if you give the path as a valid url. – jfs Aug 07 '15 at 19:14
  • 1
    macOS: `import webbrowser webbrowser.open("file:///Users/nameGoesHere/Desktop/folder/file.py")` –  Dec 17 '17 at 22:53
  • 4
    https://docs.python.org/3/library/webbrowser.html#webbrowser.open "Note that on some platforms, trying to open a filename using [webbrowser.open(url)], may work and start the operating system’s associated program. However, this is neither supported nor portable." – nyanpasu64 Mar 25 '19 at 22:39
7

If you want to go the subprocess.call() way, it should look like this on Windows:

import subprocess
subprocess.call(('cmd', '/C', 'start', '', FILE_NAME))

You can't just use:

subprocess.call(('start', FILE_NAME))

because start is not an executable but a command of the cmd.exe program. This works:

subprocess.call(('cmd', '/C', 'start', FILE_NAME))

but only if there are no spaces in the FILE_NAME.

While subprocess.call method enquotes the parameters properly, the start command has a rather strange syntax, where:

start notes.txt

does something else than:

start "notes.txt"

The first quoted string should set the title of the window. To make it work with spaces, we have to do:

start "" "my notes.txt"

which is what the code on top does.

Edward
  • 1,062
  • 1
  • 17
  • 39
Tomas Sedovic
  • 42,675
  • 9
  • 40
  • 30
5

os.startfile(path, 'open') under Windows is good because when spaces exist in the directory, os.system('start', path_name) can't open the app correctly and when the i18n exist in the directory, os.system needs to change the unicode to the codec of the console in Windows.

TheTechRobo the Nerd
  • 1,249
  • 15
  • 28
BearPy
  • 322
  • 5
  • 7
5

Start does not support long path names and white spaces. You have to convert it to 8.3 compatible paths.

import subprocess
import win32api

filename = "C:\\Documents and Settings\\user\\Desktop\file.avi"
filename_short = win32api.GetShortPathName(filename)

subprocess.Popen('start ' + filename_short, shell=True )

The file has to exist in order to work with the API call.

bFloch
  • 137
  • 2
  • 1
3

Here is the answer from Nick, adjusted slightly for WSL:

import os
import sys
import logging
import subprocess

def get_platform():
    if sys.platform == 'linux':
        try:
            proc_version = open('/proc/version').read()
            if 'Microsoft' in proc_version:
                return 'wsl'
        except:
            pass
    return sys.platform

def open_with_default_app(filename):
    platform = get_platform()
    if platform == 'darwin':
        subprocess.call(('open', filename))
    elif platform in ['win64', 'win32']:
        os.startfile(filename.replace('/','\\'))
    elif platform == 'wsl':
        subprocess.call('cmd.exe /C start'.split() + [filename])
    else:                                   # linux variants
        subprocess.call(('xdg-open', filename))
marcelovca90
  • 2,673
  • 3
  • 27
  • 34
Iakov
  • 31
  • 1
3

I am pretty late to the lot, but here is a solution using the windows api. This always opens the associated application.

import ctypes

shell32 = ctypes.windll.shell32
file = 'somedocument.doc'

shell32.ShellExecuteA(0,"open",file,0,0,5)

A lot of magic constants. The first zero is the hwnd of the current program. Can be zero. The other two zeros are optional parameters (parameters and directory). 5 == SW_SHOW, it specifies how to execute the app. Read the ShellExecute API docs for more info.

George
  • 15,241
  • 22
  • 66
  • 83
2

On windows 8.1, below have worked while other given ways with subprocess.call fails with path has spaces in it.

subprocess.call('cmd /c start "" "any file path with spaces"')

By utilizing this and other's answers before, here's an inline code which works on multiple platforms.

import sys, os, subprocess
subprocess.call(('cmd /c start "" "'+ filepath +'"') if os.name is 'nt' else ('open' if sys.platform.startswith('darwin') else 'xdg-open', filepath))
Ch.Idea
  • 578
  • 6
  • 8
2

If you want to specify the app to open the file with on Mac OS X, use this: os.system("open -a [app name] [file name]")

1

On mac os you can call open:

import os
os.open("open myfile.txt")

This would open the file with TextEdit, or whatever app is set as default for this filetype.

Itchydon
  • 2,572
  • 6
  • 19
  • 33
lcvinny
  • 300
  • 2
  • 8
1

I built a small library combining the best answers here for cross-platform support:

$ pip install universal-startfile

then launch a file or URL:

from startfile import startfile

startfile("~/Downloads/example.png")
startfile("http://example.com")
Jace Browning
  • 11,699
  • 10
  • 66
  • 90
0

I think you might want to open file in editor.

For Windows

subprocess.Popen(["notepad", filename])

For Linux

subprocess.Popen(["text-editor", filename])
0

I was getting an error when calling my open file() function. I was following along with a guide but the guide was written in windows while I'm on Linux. So the os.statrfile method wasn't working for me. I was able to alleviate this problem by doing the following:

Import libraries

import sys, os, subprocess
import tkinter
import tkinter.filedioalog as fd
import tkinter.messagebox as mb

After the lib imports I then called the subprocess method for opening a file in unix based OS which is "xdg-open" and the file that will be opened.

def open_file():
     file = fd.askopenfilename(title='Choose a file of any type', filetypes=[('All files', "*.*")])
     subprocess.call(['xdg-open', file])