Pythonic remote code execution using decorators. Run code on remote servers as if it were local, and support multiple Python versions at the same time!
- Install,
pip3 install git+https://github.com/Scicom-AI-Enterprise-Organization/pyremote- Simple execution,
from pyremote import remote
x = 10
y = [1, 2, 3]
@remote("localhost", "ubuntu", password="ubuntu123", python_path="python3.10")
def compute():
result = x * 2
y.append(result)
print(f"Computed: {result}")
return result
r = compute()
print(y) # [1, 2, 3, 20]
print(r)Output,
Computed: 20
[1, 2, 3, 20]
20
def remote(
host: str,
username: str,
port: int = 22,
password: Optional[str] = None,
key_filename: Optional[str] = None,
key_password: Optional[str] = None,
timeout: int = 30,
venv: Optional[Union[str, VenvConfig]] = None,
uv: Optional[Union[str, UvConfig]] = None,
dependencies: List[str] = None,
python_path: Optional[str] = None,
setup_commands: List[str] = None,
install_verbose: bool = False,
stdout_callback: Optional[Callable[[str], None]] = None,
) -> Callable:
"""
Decorator for remote Python function execution over SSH.
The decorated function will execute on the remote machine. Variables
from the enclosing scope are automatically captured and modifications
are synced back after execution.
Note:
Cross-Python-version execution is supported! The function source code
is sent to the remote, so you can run Python 3.10 locally and execute
on a remote Python 3.12 environment.
However, return values must be serializable across versions. For best
compatibility, return primitive types (dict, list, str, int, float).
Note:
To reassign variables from outer scope, use `global` keyword:
x = 10
@remote(...)
def compute():
global x # required for reassignment
x = x + 1
Mutable objects (lists, dicts) can be modified in-place without `global`.
Args:
host: Remote hostname or IP
username: SSH username
port: SSH port (default 22)
password: SSH password (optional if using key)
key_filename: Path to SSH private key (optional)
key_password: Passphrase for SSH key (optional)
timeout: Connection timeout in seconds
venv: Standard venv path (str) or VenvConfig object
uv: UV venv path (str) or UvConfig object (takes precedence over venv)
dependencies: List of pip packages to install on remote
python_path: Override python interpreter path
setup_commands: List of shell commands to run before execution
install_verbose: If True, stream pip/uv install output to stdout in real-time
stdout_callback: Optional callback function that receives each line of
stdout output as it streams from the remote. Useful for real-time
logging, progress tracking, or capturing output without printing.
Examples:
# Cross-version execution (local 3.10, remote 3.12)
@remote("server.com", "user", password="pass",
uv=UvConfig(path="~/.venv", python_version="3.12"),
dependencies=["numpy"])
def compute():
import numpy as np
arr = np.array([1, 2, 3])
print(f"Running on Python {__import__('sys').version}")
return arr.tolist() # return list for cross-version compatibility
result = compute()
# Stream installation output for large dependencies
@remote("server.com", "user", password="pass",
dependencies=["torch", "transformers"],
install_verbose=True)
def train():
import torch
return torch.cuda.is_available()
"""from pyremote import remote, UvConfig
@remote(
"localhost",
"ubuntu",
password="ubuntu123",
uv=UvConfig(path="~/.venv", python_version="3.10.17"),
dependencies=["numpy==1.26.4", "pandas==2.2.3"]
)
def compute(a, b=10):
import numpy as np
result = np.array([1, 2, 3]) * a + b
print(f"a={a}, b={b}, result={result}")
return result.tolist()
result1 = compute(5)
print(result1)
result2 = compute(2, b=100)
print(result2)
result3 = compute(a=3, b=50)
print(result3)Output,
a=5, b=10, result=[15 20 25]
[15, 20, 25]
a=2, b=100, result=[102 104 106]
[102, 104, 106]
a=3, b=50, result=[53 56 59]
[53, 56, 59]
Checkout examples/simple_pass_variables.py
from pyremote import remote
import numpy as np
result = np.array([1, 2, 3])
@remote("localhost", "ubuntu", password="ubuntu123", python_path="python3.10", dependencies=["numpy==1.26.4"])
def compute():
global result
result = np.concatenate([result, result])
result += 1
compute()
print(result)
compute()
print(result)Output,
[2 3 4 2 3 4]
[3 4 5 3 4 5 3 4 5 3 4 5]
Checkout examples/simple_numpy.py
from pyremote import remote, UvConfig
import numpy as np
import sys
@remote(
"localhost",
"ubuntu",
password="ubuntu123",
uv=UvConfig(path="~/.venv", python_version="3.10.17"),
dependencies=["numpy==1.26.4", "pandas==2.2.3"]
)
def compute():
import numpy as np
import pandas as pd
print('inside compute()', sys.version)
df = pd.DataFrame({'name': ['a', 'b', 'c'], 'data': np.array([1, 2, 3])})
return df
@remote(
"localhost",
"ubuntu",
password="ubuntu123",
uv=UvConfig(path="~/.venv-3.12", python_version="3.12"),
dependencies=["numpy==1.26.4", "pandas==2.2.3"]
)
def compute2():
import numpy as np
import pandas as pd
print('inside compute2()', sys.version)
df = pd.DataFrame({'name': ['a', 'b', 'c'], 'data': np.array([1, 2, 3])})
return df
result = compute()
print(result)
result = compute2()
print(result)Output,
inside compute() 3.10.17 (main, Apr 9 2025, 08:54:15) [GCC 9.4.0]
name data
0 a 1
1 b 2
2 c 3
inside compute2() 3.12.12 (main, Dec 9 2025, 19:02:36) [Clang 21.1.4 ]
name data
0 a 1
1 b 2
2 c 3
Checkout examples/simple_uv.py
from pyremote import remote, UvConfig
logs = []
def log_handler(line: str):
"""Callback that receives each line of stdout as it streams."""
logs.append(line)
# You could also send to logging, websocket, database, etc.
# Example: logger.info(line)
# Example: websocket.send(line)
@remote(
"localhost",
"ubuntu",
password="ubuntu123",
uv=UvConfig(path="~/.venv", python_version="3.12"),
timeout=60,
stdout_callback=log_handler,
)
def task_with_logging():
import time
import sys
print(f"Python version: {sys.version}")
print("Starting task...")
for i in range(5):
print(f"Processing step {i+1}/5")
time.sleep(1)
print("Task completed!")
return {"status": "completed", "steps": 5}
if __name__ == "__main__":
result = task_with_logging()
print("\n--- Captured Logs ---")
for log in logs:
print(f" > {log}")
print(f"\nResult: {result}")
print(f"Total log lines captured: {len(logs)}")Output,
Python version: 3.10.17 (main, Apr 9 2025, 08:54:15) [GCC 9.4.0]
Starting task...
Processing step 1/5
Processing step 2/5
Processing step 3/5
Processing step 4/5
Processing step 5/5
Task completed!
--- Captured Logs ---
> Python version: 3.10.17 (main, Apr 9 2025, 08:54:15) [GCC 9.4.0]
> Starting task...
> Processing step 1/5
> Processing step 2/5
> Processing step 3/5
> Processing step 4/5
> Processing step 5/5
> Task completed!
Result: {'status': 'completed', 'steps': 5}
Total log lines captured: 8
Checkout examples/simple_stdout_callback.py