Python's global package space is a shared resource. Installing test dependencies globally creates version conflicts when different projects need different versions of the same library. Virtual environments solve this: each project gets its own isolated Python environment.
Creating a virtual environment
# Create a venv in the project directory
python3 -m venv .venv
# Activate it
source .venv/bin/activate # macOS / Linux
.venv\Scripts\activate # Windows
# Confirm activation — prompt changes to (.venv)
which python # should show .venv/bin/pythonAlways activate before running any pip installs or test commands. If the prompt shows the venv name, it's active.
Installing dependencies
pip install Appium-Python-Client pytest pytest-html allure-pytestPinning with requirements.txt
requirements.txt captures the exact versions for reproducible installs:
pip freeze > requirements.txtA typical requirements.txt for an Appium suite:
Appium-Python-Client==3.1.0
selenium==4.18.1
pytest==8.1.0
pytest-html==4.1.1
allure-pytest==2.13.2
pytest-xdist==3.5.0
python-dotenv==1.0.1
For teammates and CI:
pip install -r requirements.txtrequirements.txt vs pyproject.toml
requirements.txt is the simplest approach for test-only projects. pyproject.toml with a tool like poetry or hatch is better for projects that also publish packages, but adds complexity. For a test suite with no publication needs, stick with requirements.txt.
Project directory layout
appium-python-suite/
├── .venv/ # never commit this
├── requirements.txt
├── pytest.ini # or pyproject.toml [tool.pytest.ini_options]
├── conftest.py # shared fixtures
├── pages/
│ ├── __init__.py
│ ├── base_page.py
│ ├── login_page.py
│ └── home_page.py
├── tests/
│ ├── __init__.py
│ ├── test_login.py
│ └── test_checkout.py
├── utils/
│ ├── __init__.py
│ ├── gesture_utils.py
│ └── wait_utils.py
└── apps/
├── app-debug.apk
└── MyApp.app
.gitignore entries
.venv/
__pycache__/
*.pyc
.pytest_cache/
allure-results/
screenshots/
*.log
.envNever commit .venv/ — it contains platform-specific binaries and is rebuilt from requirements.txt.
pytest.ini
Configure pytest defaults in pytest.ini:
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = --tb=short -vaddopts = --tb=short -v applies to every run. -v shows test names; --tb=short shows short tracebacks (readable without being overwhelming).
Environment variables with python-dotenv
Device names, app paths, and Appium server URLs should come from environment variables, not hardcoded in test code:
.env file (gitignored):
APPIUM_SERVER=http://127.0.0.1:4723
ANDROID_DEVICE=emulator-5554
IOS_DEVICE=iPhone 15
APP_PATH_ANDROID=apps/app-debug.apk
APP_PATH_IOS=apps/MyApp.app
Load in conftest.py:
from dotenv import load_dotenv
import os
load_dotenv()
APPIUM_SERVER = os.getenv("APPIUM_SERVER", "http://127.0.0.1:4723")
ANDROID_DEVICE = os.getenv("ANDROID_DEVICE", "emulator-5554")CI systems (GitHub Actions, GitLab CI) set environment variables natively — load_dotenv() falls back to actual environment variables when no .env file is present, so the same code works in both contexts.