2026, Jan 06 05:00

Migrating from Python’s removed pipes module: control mpg123 via stdin instead of FIFOs or pipes.Template

Fix mpg123 remote control after Python 3.13 removed the pipes module: skip fragile FIFOs, send newline-terminated commands over stdin, flush writes.

When migrating old Python utilities to modern interpreters, some legacy modules can quietly pull the rug from under your tooling. A frequent casualty is the pipes module, deprecated for years and removed in Python 3.13. If you’ve been remote-controlling mpg123 with an iTunes-like controller and suddenly lost the pipe-based workflow, the fix is simpler than it looks: talk to mpg123 over stdin instead of juggling named pipes or pipes.Template.

Legacy setup that stopped working

The original controller relied on mpg123’s remote mode and a template-based pipeline to send commands. It looked roughly like this and worked fine on older Python:

import subprocess, pipes
fifo_path = "/tmp/mpg123remote.cmd"
player_log = "/tmp/mpg123.out"
log_fp = open(player_log, "w")
player_proc = subprocess.Popen(["/usr/bin/mpg123", "-vR", "--fifo", fifo_path], stdin=subprocess.PIPE, stdout=log_fp)
tmpl = pipes.Template()
tmpl.append("echo load \"%s\"" % "test.mp3", '--')
fifo_writer = tmpl.open(fifo_path, 'w')
fifo_writer.close()

After the removal of pipes, attempting to switch to a named pipe by hand and write the command directly may look like a natural step. However, this approach can be brittle and, as many have found, may yield a silent mpg123: it initializes, selects an output device, and then nothing plays.

A non-working attempt with a named pipe

Here is a version that tries to keep using a FIFO, but ends up producing no audio and no errors, only mpg123’s startup output:

import os, io, subprocess
fifo_path = "/tmp/mpg123remote.cmd"
try:
    os.mkfifo(fifo_path)
    print("Named pipe created successfully!")
except FileExistsError:
    print("Named pipe already exists!")
except OSError as err:
    print(f"Named pipe creation failed: {err}")
log_fp = open("/tmp/mpg123.out", "w")
player_proc = subprocess.Popen(["/usr/bin/mpg123", "-a", "default", "-vR", "--fifo", fifo_path], stdout=log_fp)
fifo_fd = os.open(fifo_path, os.O_RDWR | os.O_NONBLOCK)
track_file = "test.mp3"
cmd = f"load \"{track_file}\"\n"
with io.FileIO(fifo_fd, "wb") as pipe_io:
    pipe_io.write(cmd.encode())

At this point mpg123 prints its banner and ALSA selection, but no playback starts.

What’s actually going on

The immediate blocker is the removal of the pipes module; the template-based pipeline approach is gone. Also, the module was meant for template-driven shell pipelines, which is a different concern from both named pipes and subprocess management. If a deprecation warning appeared earlier, it likely originated from a part of your program that constructed those templates, not from the explicit subprocess and FIFO handling you see above.

There are a couple of operational gotchas with mpg123’s remote interface that can bite even when the high-level approach is correct. The remote commands must be newline-terminated; otherwise mpg123 won’t parse them and won’t tell you why. When sending commands via a live stdin stream, buffering means a write may not reach the player until you flush. And using communicate() is a trap for long-running control sessions because it closes the stream when it returns, which terminates your ability to issue further commands.

A simpler, working approach: control mpg123 over stdin

The most straightforward fix is to skip named pipes entirely and drive mpg123 directly via its stdin using subprocess. This keeps the process alive, sends properly terminated commands, and ensures they’re flushed immediately:

import subprocess
log_fp = open("/tmp/mpg123.out", "w")
track_file = "song.mp3"
cmd = f"load {track_file}\n"
proc = subprocess.Popen(["/usr/bin/mpg123", "-a", "default", "-vR"],
                        stdin=subprocess.PIPE,
                        stdout=log_fp,
                        text=True)
proc.stdin.write(cmd)
proc.stdin.flush()
while True:
    pass

This arrangement leverages mpg123’s remote mode as intended: the player reads commands from stdin, you terminate each command with a newline, and you flush to avoid buffering surprises. Avoid communicate() here because it will close the pipe upon return, which stops further interaction.

Why this matters

Decommissioned modules like pipes surface at the worst possible moment in long-lived automation. Relying on stdin with subprocess is both more direct and more robust for long-running control loops than juggling FIFOs or shell templates. It also reduces the number of moving parts, which means fewer things to go wrong across distro upgrades or Python releases.

Conclusion

If your media controller broke after Python 3.13, remove the dependency on pipes and talk to mpg123 over stdin. Keep the process running, make sure each remote command ends with a newline, and flush after writing so the player sees the command immediately. If you still see a deprecation warning, search your codebase for template-driven pipeline construction that used to rely on pipes.Template, as that’s a different mechanism than the stdin approach shown above. With these adjustments, the controller regains the reliability of the original setup without relying on deprecated modules.