Skip to content

Human-Like Interactions

One of the key differentiators between successful automation and easily-detected bots is how realistic the interactions are. Pydoll provides sophisticated tools to make your automation virtually indistinguishable from human behavior.

Future Enhancements

Pydoll is continuously improving its human-like interaction capabilities. Future versions will include:

  • Variable typing speed: Built-in random intervals between keystrokes to eliminate the need for manual randomization
  • Realistic keyboard sequences: Automatic simulation of typing mistakes, backspacing, corrections, and dynamic pauses for maximum realism
  • Automatic random click offsets: Optional parameter to automatically randomize click positions within elements, eliminating manual offset calculations
  • Mouse movement simulation: Realistic cursor paths with bezier curves
  • Mouse delta events: Natural acceleration and deceleration patterns
  • Hover behavior: Realistic delays and movement when hovering
  • Scroll patterns: Human-like scroll speeds and momentum with a dedicated scroll() method
  • Timing variations: Randomized delays to avoid predictable patterns

These features leverage CDP and JavaScript capabilities for maximum realism.

Why Human-Like Interactions Matter

Modern websites employ sophisticated bot detection techniques:

  • Event timing analysis: Detecting impossibly fast or perfectly timed actions
  • Mouse movement tracking: Identifying straight-line movements or instant teleportation
  • Keyboard patterns: Spotting instant text insertion without individual keystrokes
  • Click positions: Detecting clicks always at exact center of elements
  • Action sequences: Identifying non-human patterns in user behavior

Pydoll helps you avoid detection by providing realistic interaction methods that mimic real user behavior.

Realistic Clicking

Basic Click with Simulated Mouse Events

The click() method simulates real mouse press and release events, unlike JavaScript-based clicking:

import asyncio
from pydoll.browser.chromium import Chrome

async def realistic_clicking():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com')

        button = await tab.find(id="submit-button")

        # Basic realistic click
        await button.click()

        # The click includes:
        # - Mouse move to element
        # - Mouse press event
        # - Configurable hold time
        # - Mouse release event

asyncio.run(realistic_clicking())

Click with Position Offset

Real users rarely click at the exact center of elements. Use offsets to vary click positions:

Current State: Manual Offset Calculation

Currently, you must manually calculate and randomize click offsets for each interaction. Future versions will include an optional parameter to automatically randomize click positions within element bounds.

import asyncio
import random
from pydoll.browser.chromium import Chrome

async def click_with_offset():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com/form')

        submit_button = await tab.find(tag_name="button", type="submit")

        # Click slightly off-center (more natural)
        await submit_button.click(
            x_offset=5,   # 5 pixels right of center
            y_offset=-3   # 3 pixels above center
        )

        # Currently: Manually vary the offset for each click to appear more human
        for item in await tab.find(class_name="clickable-item", find_all=True):
            offset_x = random.randint(-10, 10)
            offset_y = random.randint(-10, 10)
            await item.click(x_offset=offset_x, y_offset=offset_y)
            await asyncio.sleep(random.uniform(0.5, 2.0))

asyncio.run(click_with_offset())

Adjustable Click Hold Time

Vary the duration of mouse button press to simulate different click styles:

import asyncio
from pydoll.browser.chromium import Chrome

async def variable_hold_time():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com')

        button = await tab.find(class_name="action-button")

        # Quick click (default is 0.1s)
        await button.click(hold_time=0.05)

        # Normal click
        await button.click(hold_time=0.1)

        # Slower, more deliberate click
        await button.click(hold_time=0.2)

        # Simulate user hesitation
        await asyncio.sleep(0.8)
        await button.click(hold_time=0.15)

asyncio.run(variable_hold_time())

When to Use click() vs click_using_js()

Understanding the difference is crucial for avoiding detection:

import asyncio
from pydoll.browser.chromium import Chrome

async def click_methods_comparison():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com')

        button = await tab.find(id="interactive-button")

        # Method 1: click() - Simulates real mouse events
        # ✅ Triggers all mouse events (mousedown, mouseup, click)
        # ✅ Respects element positioning
        # ✅ More realistic and harder to detect
        # ❌ Requires element to be visible and in viewport
        await button.click()

        # Method 2: click_using_js() - Uses JavaScript click()
        # ✅ Works on hidden elements
        # ✅ Faster execution
        # ✅ Bypasses visual overlays
        # ❌ May be detected as automation
        # ❌ Doesn't trigger same event sequence as real user
        await button.click_using_js()

asyncio.run(click_methods_comparison())

Best Practice: Prefer Mouse Events

Use click() for user-facing interactions to maintain realism. Reserve click_using_js() for backend operations, hidden elements, or when speed is critical and detection isn't a concern.

Realistic Text Input

Natural Typing with Intervals

The type_text() method simulates human typing by sending individual keystrokes. The interval parameter adds a fixed delay between each keystroke.

Current State: Manual Randomization Required

Currently, the interval parameter uses a constant delay for all characters. For maximum realism, you need to manually randomize typing speeds (as shown in the advanced examples below). Future versions will include automatic variable typing speed with built-in randomization.

import asyncio
from pydoll.browser.chromium import Chrome

async def natural_typing():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com/login')

        username_field = await tab.find(id="username")
        password_field = await tab.find(id="password")

        # Type with fixed intervals (currently)
        # Average human typing: 0.1-0.3 seconds per character
        await username_field.type_text("john.doe@example.com", interval=0.15)

        # Slower typing for complex passwords
        await password_field.type_text("MyC0mpl3xP@ssw0rd!", interval=0.12)

asyncio.run(natural_typing())

Fast Input for Non-Visible Fields

For fields that don't require realism (like hidden fields or backend operations), use insert_text():

import asyncio
from pydoll.browser.chromium import Chrome

async def fast_vs_realistic_input():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com/form')

        # Realistic typing for visible fields
        username = await tab.find(id="username")
        await username.click()
        await username.type_text("john_doe", interval=0.12)

        # Fast insertion for hidden or backend fields
        hidden_field = await tab.find(id="hidden-token")
        await hidden_field.insert_text("very-long-generated-token-12345678")

        # Realistic typing for fields that matter
        comment = await tab.find(id="comment-box")
        await comment.click()
        await comment.type_text("This looks like human input!", interval=0.15)

asyncio.run(fast_vs_realistic_input())

Advanced Keyboard Control

For comprehensive keyboard control documentation, including special keys, key combinations, modifiers, and complete key reference tables, see Keyboard Control.

Combining Techniques for Maximum Realism

Complete Form Filling Example

Here's a comprehensive example combining all human-like interaction techniques. This demonstrates the current manual approach for achieving maximum realism—future versions will automate much of this randomization:

import asyncio
import random
from pydoll.browser.chromium import Chrome
from pydoll.constants import Key

async def human_like_form_filling():
    """Fill a form with maximum realism to avoid detection."""
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com/registration')

        # Wait a bit (user reading the page)
        await asyncio.sleep(random.uniform(1.5, 3.0))

        # Fill first name with variable typing speed
        first_name = await tab.find(id="first-name")
        await first_name.click(
            x_offset=random.randint(-5, 5),
            y_offset=random.randint(-5, 5)
        )
        await asyncio.sleep(random.uniform(0.2, 0.5))

        # Manual character-by-character typing with randomized delays
        # (This will be automated in future versions)
        name_text = "John"
        for char in name_text:
            await first_name.type_text(char, interval=0)
            await asyncio.sleep(random.uniform(0.08, 0.22))

        # Tab to next field
        await asyncio.sleep(random.uniform(0.3, 0.8))
        await first_name.press_keyboard_key(Key.TAB)

        # Fill last name
        await asyncio.sleep(random.uniform(0.2, 0.5))
        last_name = await tab.find(id="last-name")
        await last_name.type_text("Doe", interval=random.uniform(0.1, 0.18))

        # Tab to email
        await asyncio.sleep(random.uniform(0.4, 1.0))
        await last_name.press_keyboard_key(Key.TAB)

        # Fill email with realistic pauses
        await asyncio.sleep(random.uniform(0.2, 0.5))
        email = await tab.find(id="email")

        email_text = "john.doe@example.com"
        for i, char in enumerate(email_text):
            await email.type_text(char, interval=0)
            # Longer pause at @ and . symbols (natural)
            if char in ['@', '.']:
                await asyncio.sleep(random.uniform(0.2, 0.4))
            else:
                await asyncio.sleep(random.uniform(0.08, 0.2))

        # Simulate user reviewing what they typed
        await asyncio.sleep(random.uniform(1.0, 2.5))

        # Accept terms checkbox with offset
        terms_checkbox = await tab.find(id="accept-terms")
        await terms_checkbox.click(
            x_offset=random.randint(-3, 3),
            y_offset=random.randint(-3, 3),
            hold_time=random.uniform(0.08, 0.15)
        )

        # Pause before submitting (user reviewing form)
        await asyncio.sleep(random.uniform(1.5, 3.0))

        # Click submit with realistic parameters
        submit_button = await tab.find(tag_name="button", type="submit")
        await submit_button.click(
            x_offset=random.randint(-8, 8),
            y_offset=random.randint(-5, 5),
            hold_time=random.uniform(0.1, 0.2)
        )

        print("Form submitted with human-like behavior")

asyncio.run(human_like_form_filling())

Best Practices for Avoiding Detection

Manual Randomization Currently Required

The following best practices represent the current state of Pydoll, where you must manually implement randomization. While this requires more code, it gives you fine-grained control over behavior. Future versions will automate these patterns while maintaining the same level of realism.

1. Always Add Random Delays

import asyncio
import random
from pydoll.browser.chromium import Chrome

# Bad: Predictable timing
await element1.click()
await element2.click()
await element3.click()

# Good: Variable timing (currently required)
await element1.click()
await asyncio.sleep(random.uniform(0.5, 1.5))
await element2.click()
await asyncio.sleep(random.uniform(0.8, 2.0))
await element3.click()

2. Vary Click Positions

import asyncio
import random
from pydoll.browser.chromium import Chrome

# Bad: Always center clicks
for button in buttons:
    await button.click()

# Good: Varied positions (currently manual)
for button in buttons:
    await button.click(
        x_offset=random.randint(-10, 10),
        y_offset=random.randint(-10, 10)
    )

3. Simulate Natural User Behavior

import asyncio
import random
from pydoll.browser.chromium import Chrome

async def natural_user_simulation(tab):
    # User arrives at page
    await tab.go_to('https://example.com')

    # User reads page content (1-3 seconds)
    await asyncio.sleep(random.uniform(1.0, 3.0))

    # User scrolls down to see more
    # Currently: Manual JavaScript scroll (instant, not realistic)
    # Future: Dedicated scroll() method with human-like momentum and acceleration
    await tab.execute_script("window.scrollBy(0, 300)")
    await asyncio.sleep(random.uniform(0.5, 1.5))

    # User finds and clicks button
    button = await tab.find(class_name="cta-button")
    await button.click(
        x_offset=random.randint(-5, 5),
        y_offset=random.randint(-5, 5)
    )

    # User waits for content to load
    await asyncio.sleep(random.uniform(0.8, 1.5))

4. Combine Multiple Techniques

import asyncio
import random
from pydoll.browser.chromium import Chrome

async def advanced_stealth_automation():
    """Combine multiple techniques for maximum stealth."""
    async with Chrome() as browser:
        tab = await browser.start()

        # Use human-like page load waiting
        await tab.go_to('https://example.com/sensitive-page')
        await asyncio.sleep(random.uniform(2.0, 4.0))

        # Scroll realistically (current manual approach)
        # Future versions will have a dedicated scroll() method with momentum
        for _ in range(random.randint(2, 4)):
            scroll_amount = random.randint(200, 500)
            await tab.execute_script(f"window.scrollBy(0, {scroll_amount})")
            await asyncio.sleep(random.uniform(0.8, 2.0))

        # Find element with timeout (simulating user search)
        target = await tab.find(
            class_name="target-element",
            timeout=random.randint(3, 7)
        )

        # Click with all realistic parameters
        await target.click(
            x_offset=random.randint(-12, 12),
            y_offset=random.randint(-8, 8),
            hold_time=random.uniform(0.09, 0.18)
        )

        # Human reaction time
        await asyncio.sleep(random.uniform(0.5, 1.2))

asyncio.run(advanced_stealth_automation())

Performance vs Realism Trade-offs

Sometimes you need to balance speed with realism:

import asyncio
from pydoll.browser.chromium import Chrome

async def balanced_automation():
    """Choose appropriate realism level based on context."""
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com/scraping-target')

        # Phase 1: Initial interaction (high realism)
        # This is when detection systems are most active
        login_button = await tab.find(text="Login")
        await asyncio.sleep(random.uniform(1.0, 2.0))
        await login_button.click(
            x_offset=random.randint(-5, 5),
            y_offset=random.randint(-5, 5)
        )

        await asyncio.sleep(random.uniform(0.5, 1.0))

        username = await tab.find(id="username")
        await username.type_text("user@example.com", interval=0.12)

        await asyncio.sleep(random.uniform(0.3, 0.7))

        password = await tab.find(id="password")
        await password.type_text("password123", interval=0.10)

        submit = await tab.find(type="submit")
        await asyncio.sleep(random.uniform(0.8, 1.5))
        await submit.click()

        # Phase 2: Authenticated data extraction (lower realism, higher speed)
        # Less scrutiny after successful authentication
        await asyncio.sleep(2)

        # Fast navigation through pages
        items = await tab.find(class_name="data-item", find_all=True)

        for item in items:
            # Quick click without offsets
            await item.click_using_js()
            await asyncio.sleep(0.3)  # Minimal delay

            # Extract data
            title = await tab.find(class_name="title")
            data = await title.text

            # Fast navigation
            await tab.execute_script("window.history.back()")
            await asyncio.sleep(0.5)

asyncio.run(balanced_automation())

Monitoring and Adjusting

Test your automation's realism:

import asyncio
import random
import time
from pydoll.browser.chromium import Chrome

async def test_interaction_timing():
    """Log timing to ensure realistic patterns."""
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com/test-page')

        # Measure and log interaction timing
        elements = await tab.find(class_name="clickable", find_all=True)

        timings = []
        last_time = time.time()

        for i, element in enumerate(elements):
            await element.click(
                x_offset=random.randint(-8, 8),
                y_offset=random.randint(-8, 8)
            )

            current_time = time.time()
            elapsed = current_time - last_time
            timings.append(elapsed)

            print(f"Click {i+1}: {elapsed:.3f}s since last action")
            last_time = current_time

            await asyncio.sleep(random.uniform(0.5, 2.0))

        # Analyze timing distribution
        avg_time = sum(timings) / len(timings)
        print(f"\nAverage time between actions: {avg_time:.3f}s")
        print(f"Min: {min(timings):.3f}s, Max: {max(timings):.3f}s")

        # Good: Variable timing with realistic average (1-2 seconds)
        # Bad: Constant timing or unrealistically fast (<0.1s)

asyncio.run(test_interaction_timing())

Learn More

For more information about element interaction methods:

Master human-like interactions, and your automation will be more reliable, harder to detect, and more closely mirror real user behavior.