Skip to content

Tutorial

Text to Speech in Python on a Mac: say, pyttsx3, and More

How to do text to speech in Python on a Mac with say and subprocess, pyttsx3, local neural models, or the Murmur CLI, plus a working decision table.

Murmur11 min read
Buy $49Download free for Apple Silicon (3-day Pro trial)Intel Mac? Download here.Not sure? Apple menu > About This Mac shows your chip.

Free plan with Kokoro voices, 3,000 characters a day · 3-day Pro trial, then $49 once

Short answer: Python on a Mac can speak text in four practical ways: call the built-in macOS say command through subprocess, use pyttsx3 which drives Apple's NSSpeechSynthesizer, run a local neural text to speech model from Python, or call the Murmur CLI as a subprocess when you want a shipped model catalog with a Mac app behind it. The first two use the voices macOS provides, so they need no model files, though some voices download when you first select them, which makes them the fastest route for a script that just has to talk. The other two give you more voices and control, and in exchange you manage packages, weights, and a runtime.

This article is about calling text to speech from Python code on an Apple Silicon Mac, not about building a real-time streaming service. Murmur itself does not expose a Python library; its automation surface is a command line tool, so the Python integration pattern is subprocess plus the murmur binary. Everything below was checked against the primary documentation linked in the sources at the end.

The four routes, and which one fits your job

Most questions of the form "how do I do text to speech in Python on a Mac" have a small answer: for short, one-off speech, use say or pyttsx3. Reach for a neural model or a studio app when you need a voice you cannot get from the system, or when you want files with a repeatable voice across many sessions. The table below frames the trade.

RouteWhat it really isWhat you needBest for
say via subprocessThe macOS speech command, called from PythonNothing extra; it ships with macOSQuick scripts, file output, voices already on the machine
pyttsx3A Python wrapper over NSSpeechSynthesizer on macOSpip install pyttsx3 (its macOS dependencies, including pyobjc, come with it)One API that also runs on Linux and Windows
Local neural modelA TTS model you install and load in PythonPython packages, model weights, a runtime such as PyTorchMore voices, cloning, and control over long narration
Murmur CLI via subprocessA signed helper that drives the Murmur macOS appMurmur installed, Murmur Pro or the 3-day trial (automation is a Pro feature), and automation enabled once in SettingsA model catalog and voice library without writing model code

Calling macOS say from Python with subprocess

The say command converts text to audible speech and can either play it or write it to a file. From Python, the recommended approach in the subprocess documentation is subprocess.run() with the command passed as a list of arguments. Passing a list matters: the docs note that a sequence lets the module handle quoting and spaces in filenames, and it avoids invoking a shell. Only use shell=True when you genuinely need shell features, because the docs warn that shell injection becomes your responsibility.

import subprocess subprocess.run( ["say", "-v", "Samantha", "-r", "180", "-o", "hello.aiff", "Hello from Python"], check=True, )

Python: run macOS say as a subprocess

With check=True, a non-zero exit status raises CalledProcessError, which the Python docs describe as the exception raised when a process run with check=True returns a non-zero status. The reference page for the say command lists the flags used here: -v chooses a voice, -r sets the rate in words per minute, and -o writes an output file. Run say -v ? to list the voices installed on your Mac, and run the same command with a grep for en_ if you only want English voices.

Two behaviors of say surprise people who wire it into a pipeline. First, if the input is a terminal or no text is given, say speaks the typed input line by line and the output file only contains the last line, per the reference page. When you call it from Python, always pass the text as an argument or point -f at a file so it synthesizes the whole thing at once. Second, say picks AIFF, CAF or M4A output from the file extension, but a bare .wav name fails with a format error; for WAV you also have to pass a data format such as --data-format=LEI16@22050. The man page names the formats AIFF, caff, m4af, and WAVE. If you need a specific container, check the extension rather than assuming the default. The guide to the macOS say command goes deeper on the individual flags.

pyttsx3: one Python API over the system voices

pyttsx3 is a Python library that works offline and, on macOS, wraps Apple's NSSpeechSynthesizer, according to its PyPI page and GitHub README. The same pages carry two caveats worth reading before you commit: NSSpeechSynthesizer is deprecated by Apple, and the newer AVSpeechSynthesizer path in pyttsx3 is still described as experimental. In practice you are calling a stable but aging macOS API through a small Python wrapper. The README also notes that if init() raises a pyobjc error on a Mac, installing "pyobjc>=9.0.1" resolves it. Keep the quotes, or the shell reads the > as a redirect.

import pyttsx3 engine = pyttsx3.init() rate = engine.getProperty("rate") engine.setProperty("rate", 125) engine.setProperty("volume", 1.0) voices = engine.getProperty("voices") engine.setProperty("voice", voices[0].id) # voice order differs per Mac, so list them and pick by name engine.say("Hello World") engine.runAndWait() engine.stop() # save_to_file is in the README, but on macOS it writes AIFF data and was unreliable in testing. # For files on a Mac, use say -o or a local model instead.

pyttsx3: rate, volume, voice, and saving to a file

The library documents choosing among the voices installed in your system, controlling rate and volume, and saving the result to a file, and the snippet above follows the shape its README shows. The README documents save_to_file followed by runAndWait() for writing audio, but on macOS the driver writes AIFF data, an .mp3 name does not produce an MP3, and the call was unreliable in testing, so for files on a Mac prefer say -o or a local model. pyttsx3.speak("text") is a one line shortcut for speaking. Because pyttsx3 delegates to the operating system's speech engine, the set of available voices is whatever macOS has, not a bundled neural voice set. That is the core trade: one tiny wrapper, no model download, and the voice quality and character track the system voices you already have.

If your Python program needs to run the same code on Linux or Windows, pyttsx3 is the one option here that spans operating systems, since it also drives eSpeak and SAPI5 elsewhere. If your program only ever runs on a Mac and you want the least indirection, calling say directly is fewer layers. The Mac text to speech workflow is a useful companion when you start turning ad hoc snippets into a repeatable pipeline.

Running a local neural model from Python

The third route is to load a neural text to speech model in Python and synthesize audio yourself. This is the route that adds the most power and the most setup. You install a package, download model weights, and usually pull in a deep learning runtime. On Apple Silicon the acceleration path is typically PyTorch's MPS backend or a project's own Metal support, and support varies by model. Before choosing a model, decide what you actually need: preset voices, voice cloning, multiple languages, or control tags. The roundup of local TTS models is a starting shortlist, and each model's own repository is the only place to trust for install commands, licenses, and hardware notes.

This route is also where the environment fights back. A model that expects a CUDA GPU will not accelerate the same way on a Mac, Python version conflicts are common, and weights can be large. If your real goal is finished audio rather than model tinkering, an app that manages the models for you is a legitimate answer, and the no Python local TTS on Mac guide covers that path. Murmur, for example, is a local macOS voice studio that ships its own model catalog and runs models on the machine with no per character fees and no upload.

Calling the Murmur CLI from Python

Murmur is not a Python package, so the integration is again subprocess, this time against the murmur binary. The CLI is a signed helper that ships inside the app, and one click in Settings links it at ~/.local/bin/murmur, which has to be on the PATH that your Python process sees. Its command shape is verified from the app's CLI source and the public automation docs: murmur generate takes --text or --input and writes a .wav or .m4a with --output, and it accepts --model, --voice, --speed, --language, and --style. You enable access once in Murmur's Settings under Automation.

import subprocess subprocess.run( ["murmur", "generate", "--text", "Welcome back. Today we are building a local voice workflow.", "--voice", "af_bella", "--speed", "0.95", "--output", "audio/intro.wav"], check=True, )

Python: call the Murmur CLI from a subprocess

The CLI starts the Murmur app when needed, runs the model locally, and the help text states plainly that it never uploads your text. By default the CLI waits for generation, batch runs, and model installation to finish, and --detach returns a job ID you can poll instead. Adding --json prints machine readable output, which is easier to parse from Python than the human readable form. Re-running a command fails if the output file already exists, unless you pass --force. For multiple files, murmur batch --manifest scenes.json reads a manifest of items and produces them in one sequential job. The local TTS automation guide covers scripting that in more detail, and the automation documentation lists the full command reference.

Be honest about the trade before wiring this into a project. Murmur is a Mac app for Apple Silicon and Intel Macs running macOS 15 or later. If your production machine is Windows, Linux, or a headless CI box, this route does not apply. It also is not a library you can import and bundle; it is an external process, so your code has to handle the app being absent, not yet set up, or not licensed.

Should you use Python at all here?

  • Yes, use say through subprocess if you want speech in a few lines and do not care which system voice it uses.
  • Yes, use pyttsx3 if the same code must also run on Linux or Windows.
  • Yes, use a local neural model if you need a specific voice, cloning, or control the system voices cannot give you, and you accept the setup.
  • Yes, use the Murmur CLI from subprocess if you want a voice library and model catalog managed for you on a Mac. It needs Murmur Pro or the 3-day trial.
  • No, do not add Python at all if the job is a one-off file: the no Python route may be simpler.
  • No, do not build on a shell command if you need live, real-time streaming, because none of these routes is a streaming speech server.

What can go wrong

  • Shell injection: if you pass user text into a command, keep shell=False and pass a list of arguments, as the subprocess docs recommend. Do not build one big command string.
  • say and a pipe: when standard input is a terminal or absent, say speaks line by line and saves only the last line to the output file. Always pass text or use -f.
  • Wrong container: say writes AIFF, CAF or M4A from the file extension, and a bare .wav name fails unless you also pass --data-format, for example LEI16@22050.
  • pyttsx3 depends on the OS engine: on macOS that engine is NSSpeechSynthesizer, which Apple has deprecated, so treat long term behavior as the system's to change.
  • pyobjc errors on pyttsx3.init() are common enough that the README names the fix: pip install "pyobjc>=9.0.1", quoted so the shell does not treat > as a redirect.
  • Neural model setup: Python version conflicts and the wrong accelerator are the usual failures. Read the model's own repository for supported hardware instead of assuming Metal works.
  • Murmur CLI readiness: generation only works after automation is enabled in the app, so check murmur status and handle the not ready case in your script.

One more boundary to state clearly: this article covers text to speech, not speech to text. If you need to transcribe audio back into text, that is a different tool, and Murmur does not do it. Keep the two stages separate in your pipeline design.

Frequently asked questions

Sources

Want Python free control of the voice?

Murmur is a local voice studio for Apple Silicon and Intel Macs running macOS 15 or later. It ships its own model catalog, runs models on the machine, and exposes a command line you can call from Python. The full app is $49 one-time, with a free plan and a 3-day Pro trial, and a 7-day refund policy.

Buy Murmur · $49Download free for Apple Silicon (3-day Pro trial)Intel Mac? Download here.Not sure? Apple menu > About This Mac shows your chip.

macOS 15+ · Apple Silicon and Intel Macs · Windows beta · 7-day refund policy