Python Android Automation Without Root¶
You can automate Android from Python without rooting the device.
For many workflows, you do not need private app data, privileged system permissions, or a modified OS. You need to tap visible UI, type into fields, wait for screens, and capture enough evidence when something fails.
Handsets provides a small Python wrapper around a fast Android CLI so you can write those flows in normal Python.
Quick answer¶
Install the package, open a session, and act on visible labels:
from handsets import Session
with Session() as d:
d.tap("Sign in", visible=True, unique=True)
d.fill("Email", "you@example.com")
d.fill("Password", "secret")
d.tap("Continue", visible=True, unique=True)
d.wait(text="Dashboard", timeout="15s")
That is Python Android automation without root: normal USB debugging, no Appium server, no coordinate guessing, and no rooted device image.
Install¶
Install the Python package:
You also need adb on your path and USB debugging enabled on the Android device.
Then connect:
The device does not need root. Handsets starts a small daemon as the Android shell user through adb.
Requirements¶
You need:
- macOS or Linux on the host.
adbavailable on$PATH.- USB debugging enabled on the phone or emulator.
- A device that appears in
adb devices. - The
handsetsPython package.
You do not need root, Magisk, a custom ROM, or an installed helper app on the device.
Tap by visible text¶
The simplest useful action is a text lookup:
from handsets import Session
with Session() as d:
d.tap("Sign in", visible=True, unique=True, timeout="5s")
visible=True avoids hidden nodes. unique=True fails if more than one node matches. That is safer than tapping whichever match happens to come first.
Fill a login form¶
import os
from handsets import Session
with Session() as d:
d.tap("Sign in", visible=True, unique=True)
d.fill("Email", os.environ["APP_EMAIL"])
d.fill("Password", os.environ["APP_PASSWORD"])
d.tap("Continue", visible=True, unique=True)
d.wait(text="Dashboard", timeout="15s")
That is a complete no-root Android login flow.
No Appium server. No coordinate math. No rooted test image.
Use selectors when labels are repeated¶
Real screens often repeat labels.
Use selectors when plain text is too broad:
from handsets import Session
with Session() as d:
d.fill('EditText:below(TextView[text=Email])', "you@example.com")
d.fill('EditText:below(TextView[text=Password])', "secret")
d.tap('Button:has-text("Continue")', visible=True, unique=True)
The selector syntax is CSS-like and inspired by Playwright.
Capture screenshots on failure¶
A practical automation script should leave artifacts behind.
from handsets import Session, Timeout
with Session() as d:
try:
d.tap("Continue", visible=True, unique=True)
d.wait(text="Dashboard", timeout="15s")
except Timeout:
d.screenshot("/tmp/android-failure.jpg", size=768)
raise
For many failures, a small screenshot plus the current UI dump is enough to debug the issue.
If you also want the current UI text, call the CLI from your failure handler or use the JSON mode in a subprocess:
import subprocess
subprocess.run(["hs", "ui"], check=False)
subprocess.run(["hs", "see", "--size", "768", "/tmp/android-failure.jpg"], check=False)
Batch tight loops¶
For tight loops, avoid starting a process for every command. Use a batch context:
from handsets import Session
labels = ["One", "Two", "Three", "Done"]
with Session() as d:
with d.batch(timeout="5s", retries=2) as b:
for label in labels:
b.tap(label, visible=True)
b.wait(text="Complete")
This keeps the command path warm and reduces overhead in repeated UI actions.
When to use Python instead of shell¶
Shell is great for linear flows:
Python is better when you need:
- Branching logic.
- Structured retries.
- Integration with APIs.
- Test assertions.
- Data-driven flows.
- Better error handling.
The same device control surface works both ways.
Pytest smoke test example¶
For a simple release smoke test, wrap the session in a test:
import os
from handsets import Session
def test_login_smoke():
with Session() as d:
d.tap("Sign in", visible=True, unique=True, timeout="5s")
d.fill("Email", os.environ["APP_EMAIL"])
d.fill("Password", os.environ["APP_PASSWORD"])
d.tap("Continue", visible=True, unique=True)
d.wait(text="Dashboard", timeout="20s")
That test still behaves like a normal user flow. The phone is not rooted and the app is not granted special permissions.
Limitations¶
No-root Android automation still follows Android's security model.
Secure windows may block screenshots. App-private data remains private. Some protected settings require device-owner policy or root. That is expected.
For UI automation, though, the shell user can do a lot:
- Tap.
- Type.
- Swipe.
- Read visible UI.
- Wait for text or activities.
- Capture screenshots when allowed.
That covers most app smoke tests and agent workflows.
FAQ¶
Can Python control Android without root?¶
Yes. Python can control Android through adb and a tool like Handsets. Root is not required for normal UI actions such as tapping, typing, swiping, waiting for text, or taking screenshots when the app allows it.
Do I need Appium for Python Android automation?¶
No. Appium is useful for WebDriver-style mobile testing, but smaller Python scripts can use Handsets when the target is Android-only and the workflow is label-based.
Can this run on a real phone?¶
Yes. It works with real Android phones and emulators as long as adb devices can see the target.
Can I automate secure screens?¶
You can still interact with visible UI, but Android may block screenshots for windows that use FLAG_SECURE. That is an Android platform rule, not a Handsets-specific limitation.