I like Python, don’t get me wrong, but it’s not Microsoft PowerPoint™, which is clearly the superior software in every way. Unfortunately, my managers don’t agree with me. When I do things in PowerPoint, they ask things like, “why aren’t you working?” and “why did we ever hire you?” Alas, I’m stuck working in Python.
I scoured the internet for solutions to this problem, but unfortunately there weren’t any that I found acceptable. Jupyter has a --to slides
option, but it’s not the same. Why can’t I just output everything into PowerPoint directly? Why isn’t this the default stdout on every computer in existence?
Well, worry not, fellow Microsoft Office fans. I’ve created a Python wrapper that solves this problem: PowerPoint Stream, or @pp_stream()
for short, from my Python PowerPoint Pro (pppp
) module. Just import the wrapper, wrap it around something, and Python will stream all your output directly into PowerPoint.
from pppp import pp_stream
@pp_stream('my_file.pptx')
def my_func():
print('hello world')
if __name__ == '__main__':
my_func()
And here’s the output for the code:

Hoorah! It works! It only took two extra lines to make it happen: an import statement, and the pp_stream
. The code for this is at the bottom of the page, but I want to walk through the creation of this glorious, beautiful, perfect module.
Getting Python to Take Over PowerPoint
Python and I have at least one thing in common: We can both use PowerPoint. The difference is that I use PowerPoint by interacting with it on my computer screen, whereas Python needs to do it through the Component Object Model.
import win32com.client as win32 # pip install pywin32
from pywintypes import com_error
Now we need to actually initialize an instance of the app. You can do this pretty simply with app = win32.Dispatch('PowerPoint.Application')
, but the issue with this implementation is that it’s not Pythonic:
- Too many dots. Wordy. Pythonic code is simpler than this.
- CamelCase for a method that has a single parameter, and that parameter never changes because each time we’re pulling up PowerPoint. (This is, after all, a PowerPoint module.)
- If you want to set some properties of
app
, you need to set those on separate lines, not as a parameter when initializing it. - The object returned doesn’t make use of any of Python’s magic methods.
Ideally, we want our code to do something like this:
app = PowerPointApp(*args)
The only issue with this code is that it doesn’t work. But we can make it work! One option would be to create a very simple factory function, which resolves (most) of our issues:
def PowerPointApp(threaded=True): # Factory method
if threaded:
import pythoncom
pythoncom.CoInitialize()
app = win32.Dispatch('PowerPoint.Application')
return app
The code works and it’s pretty Pythonic. But can we do better? Yes. Without any knowledge of win32com
and no desire to read into it, you might be tempted to import contextmanager
from contextlib
and use a try/finally setup to properly tear down the app instance. Thankfully though, gencache
has a function GetClassForProgID()
that facilitates subclassing, which is preferable. We can use super()
so that we don’t need to mess too much with COM.
(Note: COM is very finicky. You have to clean up a folder called gen_py
in your temp dir and previously initialize an instance of an application to get this to work 100% of the time. Yes, I agree that it’s a mess. I’ll include the cleanup in the full version at the bottom of this post.)
PowerPoint doesn’t allow you to set app.Visible
to False
for some odd reason (you can do that for Excel and Word though?), so there isn’t much we need to do after running super().__init__()
, but it’s nice to know you could put things there if you really wanted.
_PowerPoint = win32.gencache.GetClassForProgID('PowerPoint.Application')
class PowerPointApp(_PowerPoint):
def __new__(cls):
return super().__new__(cls)
def __init__(self, threaded=True):
if threaded:
import pythoncom
pythoncom.CoInitialize()
super().__init__()
One issue with using win32.Dispatch()
is that it’s leaky: the app instance doesn’t tear down properly unless you use the .Quit()
method. Now that we’re subclassing, we can deal with this trivially:
def __del__(self):
try:
if self.Presentations.Count == 0:
self.Quit()
except com_error:
pass
Spoiler alert: Later we’re going to want to use a context manager. So let’s add that in:
def __enter__(self):
return self
def __exit__(self, exctype, excval, exctb):
self.__del__()
Finally, the app.Presentations.Add()
way of creating a new file isn’t as syntactically Pythonic as we’d like it to be. We can fix this:
def new_ppt(self):
return self.Presentations.Add()
Now our PowerPointApp()
class is about where it needs to be.
Writing in PowerPoint from Python
The next step is to figure out how to write things in PowerPoint. So I scoured through the documentation, clicked F2 in Visual Basic for Applications a few times, and did a little guesswork. Here’s what I came up with:
insert_text = 'hello world'
ppt_layout = ppt.SlideMaster.CustomLayouts(2)
new_slide = ppt.Slides.AddSlide(ppt.Slides.Count + 1, ppt_layout)
new_slide.Shapes(2).TextFrame.TextRange.Text = insert_text
It’s not pretty and it’s not Pythonic, but we can make it pretty and Pythonic by hiding the ugly stuff. That’s what functions are for.
def add_slide(ppt=None, insert_text=None):
# ugly stuff here
To recap, this is what our code looks like so far to write 'hello world'
into a PowerPoint presentation:
app = PowerPointApp()
ppt = app.new_ppt()
add_slide(ppt, 'hello world')
That’s it, three whole lines. Looks good, almost like real Python code.
Hacking the print() Statement
It’s not that hard to stick print()
into str
variables.
import io
from contextlib import redirect_stdout
f = io.StringIO()
with redirect_stdout(f):
print('hello')
print('world')
string_output = f.getvalue()
And since we can already put strings into PowerPoint, making the wrapper should be easy as pie.
…Except for one issue: we can’t tell the difference between new lines in a single print()
statement versus multiple, separate prints. It’d be nice if we could print each print()
into its own slide, and maintain the line breaks defined by the user.
assert string_output == 'hello\nworld\n'
There’s a solution to this: we can monkeypatch print
in the wrapper we’re going to make. The trick here is to choose a delimiter other than \n
. We’re going to parameterize this for users’ sake, but for our purposes we’ll be using the null char, \0
, by default. (If you want to parameterize this in the actual wrapper, that’s on you.) We also want to override the user’s end keyword argument if they assign it.
class DelimitPrint(object):
def __init__(self, delimit):
self._print = print
self.delimit = delimit or '\0'
def __enter__(self):
def custom_print(*args, **kwargs):
new_kwargs = kwargs
new_kwargs['end'] = self.delimit
self._print(*args, **new_kwargs)
builtins.print = custom_print
def __exit__(self, *args, **kwargs):
builtins.print = self._print
Combine it all, and make sure the context manager tears down correctly– we wouldn’t want to accidentally monkeypatch print
forever!
f = io.StringIO()
g = io.StringIO()
with redirect_stdout(f):
with DelimitPrint('\0'):
print('hello', end='\n')
print('world')
with redirect_stdout(g):
print('hello')
print('world')
assert f.getvalue() == 'hello\0world\0'
assert g.getvalue() == 'hello\nworld\n'
Letting the pp_stream Flow
The last step is to combine all this into the wrapper. There’s a lot of context management going on, so try to keep up with the indentation!
def pp_stream(filename):
def wrapper(func):
def print_to_pp(*args, **kwargs):
delimit = '\0'
with DelimitPrint(delimit):
with PowerPointApp() as app:
f = io.StringIO()
with redirect_stdout(f):
ppt = app.new_ppt()
try:
user_func = func(*args, **kwargs)
finally:
for s in f.getvalue().split(delimit)[:-1]:
add_slide(ppt, s)
ppt.SaveAs(filename)
ppt.Close()
return user_func
return print_to_pp
return wrapper
What’s going on? The first 3 lines and last 2 lines are typical wrapper stuff. Lines 4-8 are just all the context managers we made. Lines 9-17 is where the PowerPoint magic happens.
And we’re done!
The Full Code
Sigh, I know what you degenerates are here for. You came here for the pp_stream
, not to read. I’ll have you know I took a whole evening off of job searching to make sure my pp_stream
wrapper came out golden, so you better freaking enjoy it.
import builtins
import io
import win32com.client as win32
from contextlib import redirect_stdout
from pywintypes import com_error
_PowerPoint = win32.gencache.GetClassForProgID('PowerPoint.Application')
if _PowerPoint is None:
p = win32.gencache.EnsureDispatch('PowerPoint.Application')
if p.Presentations.Count == 0:
p.Quit()
_PowerPoint = win32.gencache.GetClassForProgID('PowerPoint.Application')
class PowerPointApp(_PowerPoint):
def __new__(cls):
return super().__new__(cls)
def __init__(self, threaded=True):
if threaded:
import pythoncom
pythoncom.CoInitialize()
super().__init__()
def __del__(self):
try:
if self.Presentations.Count == 0:
self.Quit()
except com_error:
pass
def new_ppt(self):
return self.Presentations.Add()
def __enter__(self):
return self
def __exit__(self, exctype, excval, exctb):
self.__del__()
def add_slide(ppt=None, insert_text=None):
ppt_layout = ppt.SlideMaster.CustomLayouts(2)
new_slide = ppt.Slides.AddSlide(ppt.Slides.Count + 1, ppt_layout)
new_slide.Shapes(2).TextFrame.TextRange.Text = insert_text
class DelimitPrint(object):
def __init__(self, delimit):
self._print = print
self.delimit = delimit or '\0'
def __enter__(self):
def custom_print(*args, **kwargs):
new_kwargs = kwargs
new_kwargs['end'] = self.delimit
self._print(*args, **new_kwargs)
builtins.print = custom_print
def __exit__(self, *args, **kwargs):
builtins.print = self._print
def pp_stream(filename):
def wrapper(func):
def print_to_pp(*args, **kwargs):
delimit = '\0'
with DelimitPrint(delimit):
with PowerPointApp() as app:
f = io.StringIO()
with redirect_stdout(f):
ppt = app.new_ppt()
try:
user_func = func(*args, **kwargs)
finally:
for s in f.getvalue().split(delimit)[:-1]:
add_slide(ppt, s)
ppt.SaveAs(filename)
ppt.Close()
return user_func
return print_to_pp
return wrapper
if __name__ == '__main__':
@pp_stream('test_me.pptx')
def my_function():
print('Hello!\nI\'m happy you\'re using my code.')
print('See? It works!')
my_function()
You must be logged in to post a comment.