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.
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.
| Route | What it really is | What you need | Best for |
|---|---|---|---|
| say via subprocess | The macOS speech command, called from Python | Nothing extra; it ships with macOS | Quick scripts, file output, voices already on the machine |
| pyttsx3 | A Python wrapper over NSSpeechSynthesizer on macOS | pip install pyttsx3 (its macOS dependencies, including pyobjc, come with it) | One API that also runs on Linux and Windows |
| Local neural model | A TTS model you install and load in Python | Python packages, model weights, a runtime such as PyTorch | More voices, cloning, and control over long narration |
| Murmur CLI via subprocess | A signed helper that drives the Murmur macOS app | Murmur installed, Murmur Pro or the 3-day trial (automation is a Pro feature), and automation enabled once in Settings | A 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,
)
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 nameengine.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.
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,
)
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=Falseand 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 statusand 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
How do I do text to speech in Python on a Mac?
The quickest route is to call the macOS say command with subprocess.run(), passing the voice, rate, and output file as a list of arguments. If you want a Python API that works across operating systems, use pyttsx3, which wraps NSSpeechSynthesizer on macOS. For more voices or cloning, load a local neural model, and for a managed catalog you can call the Murmur CLI as a subprocess.
Does pyttsx3 work offline on macOS?
Yes. pyttsx3 is described as fully offline, and on macOS it drives Apple's NSSpeechSynthesizer. That engine is deprecated by Apple and the newer AVSpeech path is described as experimental. If init() raises a pyobjc error, the README's fix is to install pyobjc version 9.0.1 or later.
Why does say only save the last line to my file?
When the input is a terminal, or when no text is provided, say speaks the typed input line by line and the output file contains only the last line. When you call it from Python, pass the text as an argument or use the -f flag to point at a file so the whole input is synthesized at once.
Can I call Murmur from a Python script?
Murmur has no importable Python library. Its automation surface is the murmur command line tool, so you call it with subprocess.run(). Enable automation once in Murmur Settings, then use commands such as murmur generate with --output and an optional --json flag. Murmur runs on Apple Silicon and Intel Macs with macOS 15 or later.
Should I use say or a neural model?
Use say when a system voice is enough and you want the fewest moving parts. Use a local neural model when you need a specific voice, cloning, or multi-language control, and you are willing to manage packages, weights, and a runtime. There is no single right answer; it depends on the voice you need and how much setup you will tolerate.
Is Murmur the right choice on a non-Mac system?
No. Murmur is a local voice studio for Apple Silicon and Intel Macs running macOS 15 or later. If you need to run on Windows, Linux, or a headless server, use say only on macOS, use pyttsx3 for a cross-platform wrapper, or run an open source model directly in Python.
Sources
- Python 3 documentation: subprocess, run() and security considerations
- say command reference: voice, rate, output file, and TTY behavior
- pyttsx3 on PyPI: offline engine list, macOS NSSpeechSynthesizer, rate, volume, save to file
- pyttsx3 GitHub README: engines by OS, pyobjc fix, usage examples
Try Murmur - $49 one-time. No subscriptions, no cloud, no per-character fees.
Originally published at murmurtts.com
