webdriver.Remote with UiAutomator2Options creates an Android session. The returned object is typed as WebDriver but exposes Android-specific commands through Appium's execute_script interface and specialized methods available after importing from appium.webdriver.extensions.
Creating the driver
from appium import webdriver
from appium.options import UiAutomator2Options
import os
def create_android_driver() -> webdriver.Remote:
options = UiAutomator2Options()
options.device_name = os.getenv("ANDROID_DEVICE", "emulator-5554")
options.platform_version = os.getenv("ANDROID_VERSION", "13")
options.app = os.path.abspath("apps/app-debug.apk")
options.auto_grant_permissions = True
options.no_reset = True
options.new_command_timeout = 60
return webdriver.Remote("http://127.0.0.1:4723", options=options)App lifecycle
driver = create_android_driver()
# Check if installed
installed = driver.is_app_installed("com.example.myapp")
# Activate (bring to foreground)
driver.activate_app("com.example.myapp")
# Run in background for N seconds
driver.background_app(3)
# Terminate
driver.terminate_app("com.example.myapp")
# Remove
driver.remove_app("com.example.myapp")Hardware key simulation
from appium.webdriver.extensions.android.nativekey import AndroidKey
# Back button
driver.press_keycode(AndroidKey.BACK)
# Home button
driver.press_keycode(AndroidKey.HOME)
# Recent apps
driver.press_keycode(AndroidKey.APP_SWITCH)
# Volume up/down
driver.press_keycode(AndroidKey.VOLUME_UP)
driver.press_keycode(AndroidKey.VOLUME_DOWN)Clipboard
# Set clipboard text (useful for paste-flow testing)
driver.set_clipboard_text("paste this value")
# Read clipboard
text = driver.get_clipboard_text()
print(f"Clipboard: {text}")Network simulation
# Toggle WiFi
driver.toggle_wifi()
# Toggle airplane mode
driver.toggle_airplane_mode()
# Get connection state
state = driver.network_connection
print(f"Connection state: {state}")
# 0 = airplane mode, 1 = WiFi off, 2 = WiFi only, 4 = data only, 6 = allChecking current activity (debugging locator failures)
When a NoSuchElementException occurs, print the current activity to confirm you're on the right screen:
activity = driver.current_activity
package = driver.current_package
print(f"Current: {package}/{activity}")
# Output: Current: com.example.myapp/.LoginActivityIf the activity doesn't match what your page object expects, the navigation failed before the locator was attempted.
Screenshot and screen recording
import base64
# Screenshot
driver.get_screenshot_as_file("screenshots/current_state.png")
# Or get bytes for Allure attachment
screenshot_bytes = driver.get_screenshot_as_png()
# Screen recording (Android 4.4+)
driver.start_recording_screen()
# ... run test steps ...
video_base64 = driver.stop_recording_screen()
video_bytes = base64.b64decode(video_base64)
import pathlib
pathlib.Path("recordings/test.mp4").write_bytes(video_bytes)Running shell commands via ADB
For setup operations that are faster via ADB than through the app UI:
# Grant a permission directly
driver.execute_script("mobile: shell", {
"command": "pm grant com.example.app android.permission.ACCESS_FINE_LOCATION"
})
# Clear app data (faster than full reset)
driver.execute_script("mobile: shell", {
"command": "pm clear com.example.app"
})
# Get device property
result = driver.execute_script("mobile: shell", {
"command": "getprop ro.build.version.release"
})
print(f"Android version: {result}")Typical conftest.py fixture
# conftest.py
import pytest
from appium import webdriver
from appium.options import UiAutomator2Options
@pytest.fixture(scope="function")
def android_driver():
options = UiAutomator2Options()
options.device_name = "emulator-5554"
options.app = "apps/app-debug.apk"
options.auto_grant_permissions = True
options.no_reset = True
driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
yield driver
try:
driver.quit()
except Exception:
pass # Session may already be dead if the test crashed itThe try/except around driver.quit() prevents teardown failures from masking the real test failure.