Python API¶
Besides the CLI, pymobiledevice3 is a library. Everything the CLI does is built on the same
public classes you can use directly from your own code.
Install it into your project's environment (pip install pymobiledevice3 or
uv add pymobiledevice3), connect the device over USB, and accept the Trust This Computer
prompt the first time you connect. See Installation for platform notes.
The API is asyncio-based
Connection helpers and service methods are coroutines. Run them inside an event loop,
e.g. with asyncio.run(main()).
1. Connect to a device¶
Every service is created on top of a service provider, the object that knows how to reach the
device. There are two kinds: a lockdown connection (LockdownClient, over usbmux), and an
RSD connection (RemoteServiceDiscoveryService, RemoteServiceDiscovery over an iOS 17+
tunnel). Pick based on the iOS version and the service you need:
| Connection | Python entry point | iOS | Root? | Use when |
|---|---|---|---|---|
| Lockdown (USB / Wi-Fi) | create_using_usbmux |
all | no | Classic services: AFC, app install, syslog, diagnostics, backup, profiles |
| RSD, best no-root tunnel | PreferredRsdTunnel |
17+ | no | Default for developer/DVT: the userspace tunnel, falling back to the native tunnel for iOS 17.0-17.3 on macOS |
| RSD, native (macOS) | NativeRemotedTunnel |
17+ | no | macOS only: piggybacks Apple's remoted; faster host->device, but evicts and is evicted by remoted's own connection (#1994) |
| RSD, userspace | UserspaceRsdTunnel |
17+ | no | Cross-platform in-process tunnel; the tunnel address is reachable only from this process |
RSD, tunneld |
get_tunneld_devices |
17+ | yes (daemon) | You need a shared/persistent tunnel, or an external tool (e.g. lldb) must reach the device |
Rule of thumb: need a developer/DVT service on iOS 17+? Use PreferredRsdTunnel, which picks
the best no-root transport for the host. Everything else goes over lockdown. Classic lockdown
services work over USB on every iOS version (developer services did too, until iOS 17 moved them
behind RSD). The sections below show each path.
Over USB (lockdown)¶
import asyncio
from pymobiledevice3.lockdown import create_using_usbmux
async def main():
async with await create_using_usbmux() as lockdown:
print(lockdown.all_values) # full device info dict
asyncio.run(main())
create_using_usbmux(serial=...) targets a specific device by UDID; omit it to pick the first one.
iOS 17+ developer services (tunnel)¶
Developer/DVT services on iOS 17+ require an RSD tunnel.
Preferred: PreferredRsdTunnel. It uses the in-process userspace tunnel and, on macOS, falls
back automatically to the native tunnel (piggybacking Apple's remoted) for iOS 17.0-17.3, which
the userspace tunnel cannot serve. No sudo and no separate tunneld daemon. It is an async
context manager that yields a connected RemoteServiceDiscoveryService:
import asyncio
from pymobiledevice3.remote.rsd_tunnel import PreferredRsdTunnel
async def main():
# serial=None -> first device; prefer_native=True tries the macOS native tunnel first
async with PreferredRsdTunnel(serial=None) as rsd:
print(rsd.udid, rsd.product_version)
# `rsd` now drives any developer service / DvtProvider
asyncio.run(main())
To choose the transport explicitly, use UserspaceRsdTunnel (from
pymobiledevice3.remote.userspace_tunnel, cross-platform) or NativeRemotedTunnel (from
pymobiledevice3.remote.native_tunnel, macOS only). All three tunnel classes take serial= and
work either as async context managers or through explicit aopen() / aclose() calls. Caveats:
- One userspace tunnel per process, and its device address is reachable only from this process,
so don't hand it to external tools such as
lldb. - The native tunnel's address is kernel-routable, so it has no such limitation. But on that
tunnel the device keeps a single RSD connection, which ours and
remoted's keep evicting from each other; this can leave Xcode/devicectlwithout the device.
The CLI's --userspace flag goes through establish_userspace_rsd(), which opens a userspace
tunnel and keeps it alive until the process exits. It suits short scripts that never tear the
tunnel down; embedders should prefer UserspaceRsdTunnel for explicit teardown:
from pymobiledevice3.remote.userspace_tunnel import establish_userspace_rsd
async def main():
rsd = await establish_userspace_rsd() # connected RemoteServiceDiscoveryService
print(rsd.product_version)
Alternative: reuse a running tunneld. If a privileged tunneld is already running, discover
its published tunnels instead:
import asyncio
from pymobiledevice3.tunneld.api import get_tunneld_devices
async def main():
rsds = await get_tunneld_devices()
try:
rsd = rsds[0]
print(rsd.udid, rsd.product_version)
finally:
for rsd in rsds:
await rsd.close()
asyncio.run(main())
You can also wrap an explicit address with RemoteServiceDiscoveryService((host, port)) and use it
as an async context manager. See iOS 17+ tunnels for how the tunnel is
established.
2. The service pattern¶
Nearly every service subclasses LockdownService, takes a service provider, and is an async
context manager: entering it opens the service connection and leaving it closes it. A few, like
CrashReportsManager and DvtProvider, wrap other services instead. Once you know this pattern,
the services all work the same way:
import asyncio
from pymobiledevice3.lockdown import create_using_usbmux
from pymobiledevice3.services.os_trace import OsTraceService
async def main():
async with await create_using_usbmux() as lockdown, OsTraceService(lockdown) as os_trace:
async for entry in os_trace.syslog():
print(entry.pid, entry.image_name, entry.message)
asyncio.run(main())
The same service classes accept an RSD as well, so OsTraceService(rsd) works over a tunnel too.
3. Worked examples¶
These snippets go inside an async def like main() above, with lockdown an open service
provider.
Stream syslog¶
from pymobiledevice3.services.os_trace import OsTraceService
async with OsTraceService(lockdown) as os_trace:
async for entry in os_trace.syslog():
print(f"[{entry.level.name}] {entry.image_name}: {entry.message}")
List installed apps¶
from pymobiledevice3.services.installation_proxy import InstallationProxyService
async with InstallationProxyService(lockdown) as installation_proxy:
apps = await installation_proxy.get_apps()
for bundle_id, info in apps.items():
print(bundle_id, info.get("CFBundleShortVersionString"))
Browse files over AFC¶
AFC (Apple File Conduit) gives access to the device's media directory:
from pymobiledevice3.services.afc import AfcService
async with AfcService(lockdown) as afc:
print(await afc.listdir("/"))
Run a DVT (developer) service¶
DVT services go through a DvtProvider. On iOS 17+ it needs an RSD from a tunnel. It also
needs Developer Mode enabled (iOS 16+), and on iOS < 17 the Developer Disk Image mounted
(pymobiledevice3 mounter auto-mount). Each instrument is itself an async context manager:
from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider
from pymobiledevice3.services.dvt.instruments.process_control import ProcessControl
# `rsd` is a tunnel-backed service provider (see "Connect to a device" above)
async with DvtProvider(rsd) as dvt, ProcessControl(dvt) as process_control:
pid = await process_control.launch("com.apple.mobilesafari")
print("launched pid", pid)
4. Finding the right service¶
There are dozens of services under pymobiledevice3/services/. A few common ones:
| Task | Class | Module |
|---|---|---|
| Syslog / oslog / process list | OsTraceService |
services.os_trace |
| App install / list / uninstall | InstallationProxyService |
services.installation_proxy |
| File access (media / app containers) | AfcService |
services.afc |
| Diagnostics, reboot, IORegistry | DiagnosticsService |
services.diagnostics |
| SpringBoard icons / wallpaper / orientation | SpringBoardServicesService |
services.springboard |
| Crash reports | CrashReportsManager |
services.crash_reports |
| Mount the Developer Disk Image | MobileImageMounterService |
services.mobile_image_mounter |
| DVT instruments (iOS 17+ via tunnel) | DvtProvider |
services.dvt.instruments.dvt_provider |
For the full surface, see the API reference. The Writing CLI commands guide shows how the CLI wires service providers into commands.
5. Faster imports on Python 3.15+¶
On Python 3.15+ the CLI enables PEP 810 lazy imports for
itself, scoped so that only pymobiledevice3's own imports are deferred, which roughly halves its
startup time. As a library, pymobiledevice3 never changes your process's import semantics:
importing pymobiledevice3.* from your application stays eager.
If you own the process, you can opt in to the same behavior. It saves ~0.2-0.3s on the heaviest
entry points (e.g. pymobiledevice3.remote.userspace_tunnel), and the filter keeps every other
package eager, since process-wide laziness is known to break some import hooks:
import sys
def _lazy_filter(importing: str, imported: str, fromlist: object = None) -> bool:
return importing.partition(".")[0] == "pymobiledevice3"
if hasattr(sys, "set_lazy_imports"): # Python 3.15+
sys.set_lazy_imports("all")
sys.set_lazy_imports_filter(_lazy_filter)
Run this before importing any pymobiledevice3 module. On older Pythons it is a no-op.