Skip to main content

run_with_logger

Like subprocess.run, but with the ability to pipe stdout and/or stderr to a Logger or capture each stream independently while the process is running.

What it can do

  • Run local commands.
  • Run remote commands over SSH.
  • For each output stream (stdout and stderr), choose independently whether to log, capture, or discard it.
  • Pipe bytes or files to stdin.
  • Pass environment variables to the subprocess (even via SSH if the server's AcceptEnv setting allows it).
  • Enforce configurable timeouts for local subprocesses, remote SSH commands, and SSH network operations.
  • Support many of the same arguments as subprocess.run.
  • Provide access to the running process and incrementally captured output via a context manager.

Examples

Log stderr and capture stdout

The main process will wait for the subprocess while:

  • forwarding stderr line by line to the logger
  • capturing stdout in a CompletedProcess object, which is returned when the process finishes
from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["foo", "bar"],
    logger=logger,
    stdout_action="capture",
    stderr_action="log",
    check=False,
)

print(completed.stdout)

Log stdout and discard stderr

The main process will wait for the subprocess while:

  • forwarding stdout line by line to the logger
  • discarding stderr
from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["foo", "bar"],
    logger=logger,
    stdout_action="log",
    stderr_action="discard",
    check=False,
)

Capture both streams

Use "capture" for both streams when you want the final CompletedProcess to contain bytes from stdout and stderr.

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["python", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
    logger=logger,
    stdout_action="capture",
    stderr_action="capture",
)

print(completed.stdout.decode().strip())  # "out"
print(completed.stderr.decode().strip())  # "err"

Choose the log level and decoding

from logging import INFO, getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

run_with_logger(
    args=["python", "-c", "print('hello')"],
    logger=logger,
    level=INFO,
    encoding="utf-8",
    stdout_action="log",
    stderr_action="discard",
)

Send bytes to stdin

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["python", "-c", "import sys; print(sys.stdin.read().upper())"],
    logger=logger,
    stdin_data=b"hello",
    stdout_action="capture",
    stderr_action="capture",
)

print(completed.stdout.decode().strip())  # "HELLO"

Pipe a file to stdin

from logging import getLogger
from pathlib import Path
from run_with_logger import run_with_logger

logger = getLogger(__name__)
input_path = Path("input.txt")

with input_path.open("rb") as stdin:
    completed = run_with_logger(
        args=["python", "-c", "import sys; print(sys.stdin.read())"],
        logger=logger,
        stdin_io=stdin,
        stdout_action="capture",
        stderr_action="capture",
    )

Add environment variables

extra_env is merged with the current environment.

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["python", "-c", "import os; print(os.environ['APP_MODE'])"],
    logger=logger,
    extra_env={"APP_MODE": "production"},
    stdout_action="capture",
    stderr_action="capture",
)

print(completed.stdout.decode().strip())  # "production"

Use a shell command

Like subprocess.Popen, pass a string and shell=True when you intentionally want shell parsing.

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args="printf 'hello from the shell\n'",
    shell=True,
    logger=logger,
    stdout_action="capture",
    stderr_action="capture",
)

Handle non-zero exit codes

With the default check=True, non-zero exits raise CalledProcessError. Captured streams are attached to the exception.

from logging import getLogger
from subprocess import CalledProcessError
from run_with_logger import run_with_logger

logger = getLogger(__name__)

try:
    run_with_logger(
        args=["python", "-c", "import sys; print('bad'); sys.exit(2)"],
        logger=logger,
        stdout_action="capture",
        stderr_action="capture",
    )
except CalledProcessError as error:
    print(error.returncode)       # 2
    print(error.output.decode())  # captured stdout

Set check=False when you want to inspect the return code yourself:

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["python", "-c", "import sys; print('bad'); sys.exit(2)"],
    logger=logger,
    stdout_action="capture",
    stderr_action="capture",
    check=False,
)
print(completed.returncode)       # 2
print(completed.stdout.decode())  # captured stdout

Inspect a running process

Use run_with_logger__cm if you need access to the Popen object while the process is still running. Captured streams are available as BytesIO buffers.

import time
from io import BytesIO
from logging import getLogger
from run_with_logger import run_with_logger__cm

logger = getLogger(__name__)

with run_with_logger__cm(
    args=["python", "-uc", "import time; print('one'); time.sleep(1); print('two')"],
    logger=logger,
    stdout_action="capture",
    stderr_action="capture",
) as info:
    stdout_buffer = info["stdout_buffer"]
    assert isinstance(stdout_buffer, BytesIO)

    while info["process"].poll() is None:
        partial_stdout = stdout_buffer.getvalue()
        # Do something with partial_stdout while the process runs.
        time.sleep(0.1)

completed = info["completed"]

Run a remote command over SSH

Install SSH support first:

python -m pip install "run_with_logger[ssh]"

Then pass either a connected paramiko.SSHClient or a fabric.Connection.

from contextlib import closing
from logging import getLogger
from paramiko import SSHClient
from run_with_logger import run_with_logger__ssh

logger = getLogger(__name__)
with closing(SSHClient()) as client:
    
    # Configure and connect the client as appropriate for your environment.
    
    completed = run_with_logger__ssh(
        client=client,
        command="whoami",
        logger=logger,
        stdout_action="capture",
        stderr_action="capture",
    )

print(completed.stdout.decode().strip())

run_with_logger__ssh__cm provides the same context-manager pattern for SSH channels.

Metadata

Release files for run-with-logger 1.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for run-with-logger 1.5.0
File Size Uploaded
run_with_logger-1.5.0.tar.gz 22.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for run-with-logger 1.5.0
File Interpreter ABI Platform
run_with_logger-1.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 37.5 kB

Release files / run_with_logger-1.5.0.tar.gz

Download URL run_with_logger-1.5.0.tar.gz
Size 22.9 kB
Tags Source
SHA-256 checksum
How to use checksums
5c345970272ae81ded9da6d824315e41f17f50b500211b1dccaa80036366d094
BLAKE2b-256 checksum
How to use checksums
5ecef8c4edcc8cc663aeda34d8f32a9ba9d89c14148af7f5ae5b5ee8ae0220b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 5, 2026.

Transparency log

Release files / run_with_logger-1.5.0-py3-none-any.whl

Download URL run_with_logger-1.5.0-py3-none-any.whl
Size 14.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
56470d71cc0d2abd84fade5084960d7a39f7f49c3a957c49cdd9ad3f81e82839
BLAKE2b-256 checksum
How to use checksums
36ef35338c3117a74a49d8b9411f4e60eb3b44529e843624a08d0df7eb6779af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.5.0 This release

2 release files

1.4.3

2 release files

1.4.2

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page