Device
Read the device's brand, model, OS, memory and CPU architectures as plain constants, tell a phone from a tablet, TV or desktop, get the uptime, and on Android check system features like android.hardware.camera. The web page and the desktop window fill the same names from Python's platform module, so layout code runs the same everywhere. A value a platform does not have is None, not a guess. No permissions.
- pyxpo-device
- v0.1.0a1
- Native: Swift + Kotlin
- Like expo-device

Install
Install the package into the same environment as pyxpo.
Add device to modules in your app config, then import it in your code.
[tool.pyxpo]
modules = ["device"]import pyxpo_device as DeviceNeeds Python 3.9+ and pyxpo>=0.1.0a2.
This module has native code. After you add it, run pyxpo run or pyxpo build again so it is compiled into the app. Hot reload alone is not enough.
Platforms
Quick start
import pyxpo_device as Device
from pyxpo import Text, View, component, use_async
@component
def DeviceInfo():
uptime = use_async(Device.get_uptime_async, []) # (loading, value, error)
hours = (uptime.value or 0) / 3_600_000
return View(
Text("%s %s" % (Device.brand or "", Device.model_name or "")),
Text("%s %s" % (Device.os_name, Device.os_version)),
Text("Up for %.1f hours" % hours),
Text("Tablet" if Device.device_type == Device.DeviceType.TABLET else "Phone"),
)Permissions
pyxpo adds these to Info.plist and AndroidManifest.xml for you when the module is listed.
| iOS | None. |
|---|---|
| Android | None. is_side_loading_enabled_async() needs REQUEST_INSTALL_PACKAGES on Android 8+ to return True. |
API reference
Constants
Read once at app start. They resolve when you read them, so read them inside a component or function, not into module-level variables at import. Each has a camelCase twin such as Device.modelName.
is_device→ boolFalse on the iOS Simulator and, best effort, on the Android emulator. Not for security.
brand, manufacturer→ str or None"Apple" on iOS; Build.BRAND and Build.MANUFACTURER on Android.
model_name→ str or NoneMarketing name for known iPhones, else the identifier; Build.MODEL.
model_id→ str or NoneiOS. For example "iPhone17,3".
design_name, product_name→ str or NoneAndroid. Build.DEVICE and Build.PRODUCT.
device_year_class→ int or NoneRelease year of known iPhones.
total_memory→ intBytes of installed RAM.
supported_cpu_architectures→ list[str]["arm64"] on iOS; Build.SUPPORTED_ABIS.
os_name, os_version→ str"iOS" or "iPadOS", or "Android"; for example "26.0" or "15".
os_build_id, os_internal_build_id, os_build_fingerprint→ str or NoneKernel OS build on iOS; Build.DISPLAY, Build.ID and Build.FINGERPRINT on Android.
platform_api_level→ int or NoneAndroid API level.
device_name→ str or NoneThe name in the device settings. iOS 16+ returns a generic name such as "iPhone".
device_type→ DeviceTypeFrom the interface idiom on iOS; from TV mode and a 600dp width on Android.
Functions
await get_device_type_async()→ DeviceTypeSame as the constant.
await get_uptime_async()→ intMilliseconds since boot, not counting deep sleep.
await get_max_memory_async()→ intAndroid: the runtime memory limit. iOS and the desktop: MAX_SAFE_INTEGER (no limit).
await is_rooted_experimental_async()→ boolA best-effort jailbreak or root check. Easy to fool; never use it as a security decision.
await is_side_loading_enabled_async()→ boolAndroid: may this app install other apps. False elsewhere.
await get_platform_features_async()→ list[str]Android: every system feature. [] elsewhere.
await has_platform_feature_async(feature)→ boolAndroid, for example "android.hardware.camera". False elsewhere. An empty or non-string feature raises ValueError.
Types
Results and options are plain Python dicts with these keys.
DeviceType (IntEnum)
| Key | Type | Description |
|---|---|---|
UNKNOWN | 0 | The platform could not tell. |
PHONE | 1 | A phone. |
TABLET | 2 | An iPad, or Android 600dp and wider. |
DESKTOP | 3 | The web page or the desktop window. |
TV | 4 | Android TV mode. |
Constants and errors
| Key | Type | Description |
|---|---|---|
MAX_SAFE_INTEGER | int | 2 ** 53 - 1, what get_max_memory_async returns with no limit. |
ValueError | error | has_platform_feature_async got an empty or non-string feature. |
ModuleNotLinkedError | error | "device" is missing from modules. Add it and rebuild. The desktop is exempt. |
Examples
Tablet layout and hardware checks
Pick a column count from the device type, and show a camera button only where Android reports a camera.

import pyxpo_device as Device
async def layout():
columns = 3 if Device.device_type == Device.DeviceType.TABLET else 1
has_camera = await Device.has_platform_feature_async("android.hardware.camera")
return columns, has_cameraRecipe: a diagnostics line for support
One line the user can paste into a support email.
import pyxpo_application as Application
import pyxpo_device as Device
def diagnostics() -> str:
gb = (Device.total_memory or 0) / 1024 ** 3
parts = [
"%s %s" % (Device.brand or "?", Device.model_name or "?"),
"%s %s" % (Device.os_name, Device.os_version),
"%.1f GB" % gb,
"app %s" % (Application.native_application_version or "dev"),
]
if Device.platform_api_level:
parts.append("API %s" % Device.platform_api_level)
if not Device.is_device:
parts.append("emulator")
return " | ".join(parts)Testing
Patch _all to control the constants in a unit test.
import pyxpo_device as Device
def test_tablet_layout(monkeypatch):
monkeypatch.setattr(Device, "_all", lambda: {**{k: None for k in Device._FIELDS}, "deviceType": 2})
assert Device.device_type == Device.DeviceType.TABLETGood to know
- No module options and no config plugin.
- On the web and desktop, brand and model are None, total_memory comes from sysconf where available, and the uptime uses the monotonic clock.
- total_memory is installed RAM. Free memory changes every second and is not something the system promises to report.
- Do not send device_name to your server without the user knowing.