2026-02-15 12:11:58
If you're building a Django application with user authentication, you'll need a way for users to reset their passwords when they forget them.
Fortunately, Django comes with a built-in password reset system that handles the heavy lifting for you.
In this tutorial, I'll show you how to implement a complete password reset workflow in your Django application.
By the end of this guide, you'll have a fully functional password reset system that sends secure emails to users and allows them to reset their passwords safely.
Django's django.contrib.auth module provides everything you need for password reset functionality. The system uses four main class-based views that handle the entire workflow:
Here's how the workflow works from a user's perspective:
The beauty of this system is that Django handles token generation, validation, and security for you. The tokens are time-sensitive (valid for 3 days by default) and can only be used once.
Note: Django doesn't reveal whether an email exists in the system when a reset is requested. This security feature prevents potential attackers from discovering valid email addresses.
Before we dive in, make sure you have:
SendLayer offers reliable email delivery for your Django applications. If you're looking to test your emails, SendLayer offers a generous free plan that lets you send up to 200 emails.
They also offer an affordable pricing plan that scales depending on your business needs.
Start your free trial at SendLayer
Before getting started, you'll need to configure Django to send emails. There are two main approaches: a console backend for development and SMTP for production.
For local development, the console backend prints emails to your terminal rather than sending them. This is perfect for testing.
To use the console backend, open your settings.py file, and add the following snippet to it:
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
With this configuration, when you trigger a password change, the email content (including the reset link) will appear in your terminal.
For production, you'll need an actual email service. I'll show you how to configure SendLayer's SMTP server, but the process is similar for other providers.
First, add your SMTP settings to settings.py:
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.sendlayer.net'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-smtp-username'
EMAIL_HOST_PASSWORD = 'your-smtp-password'
DEFAULT_FROM_EMAIL = '[email protected]'
Pro Tip: Do not store sensitive information such as usernames and passwords in your codebase. Use environment variables instead.
Here's how to secure sensitive credentials on your project:
Start by creating a .env file in your project's root directory. Then add your SMTP credentials.
EMAIL_HOST_USER = 'your-sendlayer-username'
EMAIL_HOST_PASSWORD = 'your-sendlayer-password'
Important: Add
.envto your.gitignorefile to prevent committing sensitive credentials to version control.
After that, you'll need to install a third-party library using the command below:
pip install python-decouple
Next, return to your settings.py file and import the config module from decouple.
from decouple import config
The config method lets you access the SMTP username and password you specified in the .env file. Here's the updated email configuration settings:
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.sendlayer.net'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = '[email protected]'
Note: If you're using SendLayer, your sender email must match the domain you've authorized in your SendLayer account. For example, if you authorized
example.com, your sender email should be[email protected].
Here's how to retrieve your SMTP server details on SendLayer. Start by logging into your account area. Once there, navigate to Settings » SMTP Credentials tab.
You'll find your SMTP credentials. The SMTP host and port number are the same for all users.
Go ahead and copy the username and password. Then return to the .env file and update the dummy details with your actual credentials.
Now, let's configure the URL patterns for our password change flow. Django's built-in views make this straightforward.
In your urls.py, add the following patterns:
# urls.py
from django.contrib.auth import views as auth_views
from django.urls import path
urlpatterns = [
path('password-reset/',
auth_views.PasswordResetView.as_view(
template_name='registration/password_reset_form.html'
),
name='password_reset'),
path('password-reset/done/',
auth_views.PasswordResetDoneView.as_view(
template_name='registration/password_reset_done.html'
),
name='password_reset_done'),
path('reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(
template_name='registration/password_reset_confirm.html'
),
name='password_reset_confirm'),
path('reset/done/',
auth_views.PasswordResetCompleteView.as_view(
template_name='registration/password_reset_complete.html'
),
name='password_reset_complete'),
]
In the code above, we first import the authentication views from django.contrib.auth. Then we define the URL patterns for each password reset view.
The line auth_views.PasswordResetView.as_view() contains the logic for the password reset form and email notification. It accepts template_name as a parameter. This is where you specify the path to your password change form.
Each reset URL pattern follows the same format. First, you define the path, then specify the specific view, and map the template to handle it.
The reset confirmation URL includes two parameters:
These parameters ensure that only the intended user can reset their password, and only within the valid timeframe.
For a Django custom password reset experience that matches your application's branding, you'll need to create custom templates.
Based on the URL pattern we defined above, Django will look for the password change templates in a registration folder within your templates directory. Let's create all the necessary templates.
First, make sure your TEMPLATES setting in settings.py includes your templates directory:
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Now create the following directory structure:
project/
├── templates/
│ └── registration/
│ ├── password_reset_form.html
│ ├── password_reset_email.html
│ ├── password_reset_subject.txt
│ ├── password_reset_done.html
│ ├── password_reset_confirm.html
│ └── password_reset_complete.html
File: templates/registration/password_reset_form.html
Note: This section assumes you already have a
base.htmltemplate.
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h2>Forgot Your Password?</h2>
<p>Enter your email address below, and we'll send you a link to reset your password.</p>
<form method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger">
{% for field in form %}
{% for error in field.errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div class="form-group">
<label for="{{ form.email.id_for_label }}">Email Address</label>
{{ form.email }}
</div>
<button type="submit" class="btn btn-primary">Send Reset Link</button>
</form>
<p class="mt-3">
<a href="{% url 'login' %}">Back to Login</a>
</p>
</div>
{% endblock %}
This template displays the initial password reset request form. It includes {% csrf_token %} for security and loops through any form errors to display validation messages. The form renders Django's email field and submits via POST to trigger the reset link email.
This is the actual email content that's sent to users when they request a password reset. You can customize the content to match your brand.
File: templates/registration/password_reset_email.html
{% autoescape off %}
Hello,
You recently requested to reset your password for your account. Click the link below to reset it:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
If you didn't request a password reset, you can safely ignore this email. Your password won't be changed unless you click the link above and create a new one.
This link will expire in 3 days.
Thanks,
The {{ site_name }} Team
{% endautoescape %}
This template generates the password reset email body. The {% autoescape off %} tag prevents HTML encoding since this is a plain-text email. Django automatically provides variables like {{ protocol }}, {{ domain }}, {{ uid }}, and {{ token }} to construct the secure reset URL.
File: templates/registration/password_reset_subject.txt
Password Reset Request
This is a simple one-line file that sets the email subject. Django uses this template to determine what appears in the reset email's subject line.
File: templates/registration/password_reset_done.html
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h2>Password Reset Email Sent</h2>
<p>
We've sent you instructions for resetting your password to the email address you submitted.
</p>
<p>
You should receive the email shortly. If you don't see it, please check your spam folder.
</p>
<p>
If you don't receive an email, make sure you've entered the address you registered with.
</p>
<p class="mt-3">
<a href="{% url 'login' %}">Return to Login</a>
</p>
</div>
{% endblock %}
This template displays after a user submits the reset request form. It confirms that the email was sent and provides helpful tips about checking spam folders. For security, it shows the same message whether the email exists or not.
File: templates/registration/password_reset_confirm.html
{% extends 'base.html' %}
{% block content %}
<div class="container">
{% if validlink %}
<h2>Enter Your New Password</h2>
<form method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger">
{% for field in form %}
{% for error in field.errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div class="form-group">
<label for="{{ form.new_password1.id_for_label }}">New Password</label>
{{ form.new_password1 }}
</div>
<div class="form-group">
<label for="{{ form.new_password2.id_for_label }}">Confirm Password</label>
{{ form.new_password2 }}
</div>
<button type="submit" class="btn btn-primary">Reset Password</button>
</form>
{% else %}
<h2>Invalid Reset Link</h2>
<p>
The password reset link was invalid, possibly because it has already been used or has expired.
</p>
<p>
Please request a new password reset.
</p>
<p class="mt-3">
<a href="{% url 'password_reset' %}">Request New Reset Link</a>
</p>
{% endif %}
</div>
{% endblock %}
This template displays when users click the reset link in their email. The {% if validlink %} check verifies the token is still valid and hasn't been used. If valid, it shows the new password form; otherwise, it displays an error message with a link to request a new reset.
File: templates/registration/password_reset_complete.html
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h2>Password Reset Complete</h2>
<p>
Your password has been successfully reset. You can now log in with your new password.
</p>
<p class="mt-3">
<a href="{% url 'login' %}" class="btn btn-primary">Log In</a>
</p>
</div>
{% endblock %}
This template displays after a successful password reset. It confirms the password was changed and provides a direct link to the login page so users can sign in with their new credentials.
Now that everything is set up, let's test the complete flow.
Here's how to test your password reset implementation if you're using the console backend. Start by running your Django development server:
python manage.py runserver
After that, open your browser and navigate to http://localhost:8000/password-reset/
Then enter a valid email address from your database and submit the Django forgot password form.
After submitting the form, check your terminal. You should see the email content printed there.
Go ahead and copy the reset link from the terminal output and paste it into your browser. You'll be redirected to the new password form if the token is valid.
After completing the password reset, you'll be directed to the success page. From here, you'll be able to log back in to your account using the new password.
Once you've verified the flow works locally, switch to your SMTP backend in settings.py. If you were using the console backend for testing, you'll need to update the EMAIL_BACKEND to use an SMTP server.
After updating the settings, navigate to the password reset route and request a password reset for a real email address.
You'll see a generic password request email sent notification.
Go ahead and check your inbox for the password change email.
If you don't find it in your main inbox, check the spam folder.
Pro Tip: Using an API-based email provider like SendLayer improves your email's deliverability and protects your domain's reputation. This ensures transactional emails get delivered to the user's inbox.
You can then use the reset link in the email to update your password.
Congratulations! You now have a fully functional password reset system in your Django app with email notification.
These are answers to some of the issues I encountered and how to resolve them.
The Django password reset email not sending issue often occurs due to missing settings or invalid SMTP credentials.
If you're not receiving reset emails:
EMAIL_USE_TLS to False to allow sending emails from non-HTTPS domains.Pro Tip: Make sure to update the
EMAIL_USE_TLSsettings when moving to production.
This error indicates Django was unable to find the password reset template at the location you set when configuring the URL pattern.
If you see TemplateDoesNotExist: registration/password_reset_form.html:
TEMPLATES setting includes the templates directory:
# settings.py
'DIRS': [BASE_DIR / 'templates'],
templates/registration/.INSTALLED_APPS. Your app should come before django.contrib.admin so Django finds your templates first.If users see "The password reset link was invalid", it often indicates:
settings.py file:
# settings.py
PASSWORD_RESET_TIMEOUT = 86400 # 24 hours in seconds
Django's password reset system is secure by default, but here are some additional best practices:
Configure password validators in your settings.py:
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 8,}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
Always use HTTPS for password reset links in production:
# settings.py (production)
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
Consider logging password reset attempts to detect suspicious activity:
import logging
logger = logging.getLogger(__name__)
# In your custom view
logger.info(f"Password reset requested for email: {email}")
These are answers to some of the top questions we see about implementing password change in Django.
While this tutorial covers Django's template-based password reset, you can implement password reset Django Rest Framework functionality using DRF's APIViews. You'll need to create API endpoints that trigger the same password reset flow, but return JSON responses instead of rendering templates.
Alternatively, consider using the dj-rest-auth package, which provides ready-made password reset endpoints for REST APIs. The token generation and validation logic remains the same.
Yes, Django's built-in password reset system works with custom user models as long as they inherit from AbstractBaseUser and include an email field. The password reset views automatically detect your custom user model through the AUTH_USER_MODEL setting. No additional configuration is needed—the Django password reset token generator will work with any properly configured user model.
This depends on your use case. If you need additional features, such as social authentication, Django-allauth is an excellent choice and handles password resets automatically.
However, if you only need basic password reset functionality, Django's built-in system is simpler and requires no additional dependencies.
In this comprehensive tutorial, we've walked through everything you need to implement a secure password reset system in Django.
Have questions or want to share your Django password reset implementation? Drop a comment below—I'd love to hear how you're handling authentication in your Django projects!
Ready to implement this in your application? Start your free trial at SendLayer and send your first password reset email in minutes.
2026-02-15 12:10:14
Why You Should Stop Worrying and Start Building?
Hey there, young hustlers and code curious! 🚀 Tired of all the doom-scrolling on social media? "AI agents are gonna steal your job!" "Everything's automating away!" Chill out. These thoughts belong to U G Murthy, paraphrased by AI – and here's the real tea: AI agents aren't the villain in your story. They're your sidekick, ready to level up your game. The future isn't about fearing them; it's about mastering how to work with them. That's the superpower skill that'll keep you ahead as AI models get smarter and cheaper every day. Think Clawdbot or Moltbot going viral, or Claude CoWork shaking up stocks – more game-changers are coming. Time to gear up and build!
Picture agents like levels in your favorite video game – from noob to boss mode:
Agents don't judge – they don't know you're "stupid" (your words, not mine!). They're always down to help, explain stuff, brainstorm wild ideas, teach you new tricks, or consult like a pro. Bonus: They mutate and evolve solo, and you can make 'em play any role – chef, coder, storyteller, you name it.
Agents are smart, but they're not mind-readers. Your success = their success. Here's how to team up like pros:
Nail this, and you're unstoppable.
Working with agents? It's a cheat code for life:
I retired in 2016 with dusty programming skills from my youth – off coding for 20+ years. But hey, time on my hands? I dove back in, learned fresh, and loved it. Fast-forward to 2025: Coding agents popped up. I tried 'em all – CoPilot, Bolt, Cursor, Windsurf, Kilo Code (solid for a bit), then boom, AmpCode stole my heart.
Got hyped from blogs by Lex Fridman and Dwarfish, but my fave? The Latent Space podcast The AI Engineer Podcast – that's where agents hooked me. By 2026, I was building one without a plan. Every step sparked new ideas, like prizes for grinding the last level. Sleepless nights? Sure. But waking up buzzing with vibes? Priceless. Slowly, I shipped desiAgent, an SDK for agents – mostly powered by AmpCode, which I used to paraphrase this post.
This Lex Fridman Podcast hit me mid-walk, and was the inspiration for doing this post.
My thoughts 💭 - the prompt used for this post
The following is a list of thoughts for an essay - I want you convert this to a an essay jovial to natural tone easily readable and targeted mostly to youngster. Write the final essay to soul-of-an-agent-v1.md
The inspiration for writing this has come from a podcast: https://podcasts.apple.com/in/podcast/lex-fridman-podcast/id1434243584?i=1000749366733 and lesson from my personal jouney
Ensure you mention somewhere : that the though are mine (U G Murthy) Paraphrased by AI
### Soul of an Agent
### Key Message to drive home
AI Agents are not all gloom and doom as being discussed on most social media. Jobs will vanish etc. Gear your self develop a very important skill - How work with agent and this requires us to understand what an agent is and how should I go about build my skill to use Agent. The AI models landscape is changing fast both in terms of capability and reducing cost. But its core is till the same allowing individuals to creating interesting use cases the one to go most viral is Clawdbot or Moltbot now and the other that crashed tech stocks is Claude CoWork and there will be more.
### Outline:
Following is an outline that needs to paraphrase and arrange in a logical flow
- Spectrum of agents
- Simple - 1 chat conversation, chain of conversations
- Moderate - Search, extract, Analyse
- All of above + more tools + Agent workflows
- Agent does not know I am stupid
- Always there to help, explain, brainstorm, teach, consult
- Agent can be mutate on its own
- Agent can play different roles
- Every Agent needs help to succeed - your success lies in there
- Show the the agent - build relevant context
- Consider this and that - give it some direction
- Do this… - expose your goal
- Provide options - make it think about alternatives
- Ask questions - Help the agent reflect
- How does it benefit me?
- Exposure to new solutions
- You get a sounding board making you reflect
- Learn on the way and that too very quickly
- Play - journey is more imp that end goal
- Learn new things
- Don’t forget you have an agent to help you - be brave , take risks in building the unknown
- Personal journey
- I have been building ever since I retired in 2016. Though I had programming experience when I was younger, I was off coding for more than 2 decades. I had all the time in the world to learn - so I did
- Glad I pursued learning to code. 2025, coding agents started to appear on the horizon and I embraced it -built and Ditched many projects. Tried Co Pilot, bolt, cursor, windsurf, settled on kilo code for a couple months and then I discovered ampCode
- Got inspired by many blog posts from Lex Freidman, Dwarfish,
- favrouite podcast remains https://www.latent.space/podcast This is where I got curious about agents
- 2026 I just started building an agent without a plan or a direction - every step in this direction gave birth to new ideas, it almost felt like a prize for the effort of previous step - there were sleepless nights but the journey was fun. I woke up every day with new ideas buzzing, it was hard to implement but slowly and steadily I build it
- Here is desiAgent an SDK for agents : Most of the work was Ampcode, I review and understand its core.
- Sharing my high level learnings of working fearlessly with agents by understanding their Soul
- I got to writing this outline for this article while on a walk listening to Lex Friedman’s pod OpenClaw - https://podcasts.apple.com/in/podcast/lex-fridman-podcast/id1434243584?i=1000749366733
- Health warning:
- AI Agents can consume you - beware of how much time and money you spend here
- Do not take your eye of from - if you want to enjoy the future
- The number of hours you sleep
- Taking action to stay fit
-
- Conclusion
- If you are young or retired dev wakeup / shapeup and start building stuff -
Agents are addictive AF – time and cash can vanish. Keep your eyes on the prize for that epic future:
Balance or burn out, fam.
Young guns or retired devs – this is your call to action. Stop scrolling, start shipping. Grab an agent, team up, and build cool stuff. The world's your playground. What's your first project? Go wild! 🌟
2026-02-15 12:06:12
Why your 'while' loop skips items—and two ways to fix it without copying.
Timothy was feeling confident. He had spent the morning cleaning up his lists using Margaret’s "Snapshot" method. But as he sipped his coffee, a thought bothered him.
"Margaret," he called out, "the snapshot copy works great for my small task list. But what if I had a list with ten million items? Creating a full copy just to delete a few things feels… expensive."
Margaret nodded, impressed. "You’re thinking about memory efficiency, Timothy. That’s the mark of a growing engineer. If each item in that list was a pointer, a snapshot would cost you about 80 megabytes of extra RAM. The manual way? It costs zero."
"I wonder if we could use a while loop," Timothy continued, showing her his screen. "It’s not as elegant as a list comprehension, but it should work without a copy. I tried to write one, but I think I broke Python."
He pointed to his console, where the cursor was blinking frantically in a sea of never-ending output.
"I tried to use a while loop to manually walk through the list," Timothy explained. "But it just keeps printing the same thing forever."
He showed her his attempt:
# Timothy's Infinite Loop
tasks = ["done", "todo", "done", "done", "todo"]
i = 0
while i < len(tasks):
if tasks[i] == "done":
tasks.remove("done")
# Timothy's logic: Just keep moving!
i += 1
print(f"End: {tasks}")
"Look at the logic," Margaret said gently. "You are moving your index forward (i += 1) every single time. But when you remove an item, the list shifts toward you. You’re stepping forward while the sidewalk is moving backward."
"If I remove the item at index 0," Timothy realized, "the item that was at index 1 is now the new index 0. If I move to index 1, I’ve jumped over it!"
"Exactly," Margaret said. "In a for loop, the 'driver' is automatic—it always steps forward. In a while loop, you are the driver. You only shift gears when it’s safe."
She helped him rewrite the logic, replacing .remove() with .pop().
tasks = ["done", "todo", "done", "done", "todo"]
i = 0
while i < len(tasks):
if tasks[i] == "done":
# We use .pop(i) because we already know the exact index.
# It's faster than searching the whole list again with .remove()
tasks.pop(i)
# CRITICAL: We do NOT increment 'i' here.
# The next item just slid into our current position!
else:
i += 1 # Only move forward if we didn't delete anything
"Now," Margaret explained, "if you find a 'done' task, you delete it and stay exactly where you are. You look at the same spot again to see what slid into it. You only move the pointer i when you're sure the current item is one you want to keep."
"Is there a way to use a for loop without a copy?" Timothy asked.
"There is one trick," Margaret smiled. "The Reverse Commute. If you start at the end of the list and walk toward the beginning, the shifting doesn't matter."
She wrote out a range that looked like a secret code: range(len(tasks) - 1, -1, -1).
"That's range(start, stop, step)," she explained. "We start at the last index, stop just before -1, and step backward by 1."
# Walking backwards
tasks = ["done", "todo", "done", "done", "todo"]
for i in range(len(tasks) - 1, -1, -1):
if tasks[i] == "done":
tasks.pop(i)
"Think about it," Margaret said. "When you remove an item at the end, the items before it—the ones you haven't visited yet—stay exactly where they are. You're removing the rug from behind you, not from under your feet."
Margaret flipped to a new page in her notebook.
tasks[:]).Timothy closed his laptop. "I used to think loops were just about repeating things. Now I realize they’re about navigating a changing world."
"That," Margaret said, "is the difference between a coder and an engineer."
In the next episode, Margaret and Timothy will face "The Lying Truth"—where Timothy learns that in Python, even a 'Zero' can be a lie, and 'Nothing' can be quite something.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
2026-02-15 11:56:40
I've been using playwright-cli — Microsoft's command-line tool that gives AI agents browser automation skills via the Playwright MCP daemon. It's brilliant for agents: one command, one process, one result. But for humans, every command spawns a brand-new Node.js process — connect to the daemon, send one message, disconnect, exit. That's 50–100ms overhead per command.
I wanted something faster. Something interactive. Something designed for humans instead of AI agents — a persistent session with instant feedback.
So I built playwright-repl — a REPL that reuses playwright-cli's command vocabulary and MCP daemon architecture, but replaces the one-shot client with a persistent socket connection. Same wire protocol, same daemon, same browser commands — just a better interface for interactive use.
$ playwright-repl --headed
pw> goto https://demo.playwright.dev/todomvc/
pw> fill "What needs to be done?" "Buy groceries"
pw> press Enter
pw> fill "What needs to be done?" "Write tests"
pw> press Enter
pw> check "Buy groceries"
pw> verify-text "1 item left"
✓ Text "1 item left" is visible
No imports. No async/await. No page.locator(). Just type what you want the browser to do.
Playwright's codegen is great for generating test code. But sometimes you don't want code — you want to explore.
.pw file and check the exit code.playwright-repl sits between "clicking around manually" and "writing a full test suite." It's the exploratory middle ground.
The thing I'm most proud of is text locators. Instead of inspecting elements for CSS selectors or waiting for a snapshot to get element refs, you just use the text you see on screen:
pw> click "Get Started"
pw> fill "Email" "[email protected]"
pw> fill "Password" "secret123"
pw> click "Sign In"
pw> check "Remember me"
pw> select "Country" "Japan"
Under the hood, it tries multiple strategies — getByText, getByRole('button'), getByRole('link'), getByLabel, getByPlaceholder — with a fallback chain. Case differences don't matter because role matching is case-insensitive.
You can also use element refs from snapshot output if you prefer precision:
pw> snapshot
- heading "todos" [ref=e1]
- textbox "What needs to be done?" [ref=e8]
pw> click e8
pw> fill e8 "Buy groceries"
This is where it gets really useful. Record your browser session as a .pw file:
pw> .record smoke-test
⏺ Recording to smoke-test.pw
pw> goto https://demo.playwright.dev/todomvc/
pw> fill "What needs to be done?" "Buy groceries"
pw> press Enter
pw> verify-text "1 item left"
pw> .save
✓ Saved 4 commands to smoke-test.pw
The .pw file is just plain text:
# CI smoke test
goto https://demo.playwright.dev/todomvc/
fill "What needs to be done?" "Buy groceries"
press Enter
verify-text "1 item left"
Replay it any time:
# Headless (CI mode)
playwright-repl --replay smoke-test.pw --silent
# With a visible browser
playwright-repl --replay smoke-test.pw --headed
# Step through interactively
playwright-repl --replay smoke-test.pw --step --headed
These .pw files are human-readable, diffable, and version-controllable. Commit them alongside your code. Run them in CI. Share them with teammates who don't write JavaScript.
No test framework needed. Verify state inline:
pw> verify-text "1 item left"
✓ Text "1 item left" is visible
pw> verify-element heading "todos"
✓ Element heading "todos" is visible
pw> verify-value "Email" "[email protected]"
✓ Value matches
If an assertion fails, you get a clear error — and in replay mode, the process exits with code 1. That's all CI needs.
Every command has a short alias for quick typing:
| You type | It does |
|---|---|
g https://example.com |
Navigate to URL |
s |
Accessibility tree snapshot |
c e5 |
Click element ref e5 |
f "Email" "[email protected]" |
Fill a form field |
p Enter |
Press a key |
ss |
Take a screenshot |
vt "hello" |
Verify text is visible |
back |
Go back in history |
The full list includes interaction (click, fill, type, press, hover, drag), inspection (snapshot, screenshot, eval, console, network), storage (cookies, localStorage, sessionStorage), tabs, dialogs, network routing, and more.
playwright-repl stands on the shoulders of playwright-cli and the Playwright MCP architecture. It reuses:
[email protected]+ — browser launch, CDP communication, all 50+ tool handlersclick, fill, snapshot, screenshot, etc.The REPL is just a thin, persistent client that:
Because the socket stays open, there's zero startup overhead per command. The daemon doesn't care whether the message came from playwright-cli, the REPL, or an AI agent — the wire messages are identical. playwright-repl adds the human-friendly layer on top: text locators, recording, replay, assertions, and aliases.
# .github/workflows/smoke.yml
jobs:
smoke-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install chromium
- run: npx playwright-repl --replay tests/smoke.pw --silent
The --silent flag suppresses the banner. The process exits 0 on success, 1 on failure. That's it.
npm install -g playwright-repl
npx playwright install
playwright-repl --headed
Then type goto https://demo.playwright.dev/todomvc/ and start exploring.
We're also building playwright-repl-extension — a Chrome DevTools panel that brings the same REPL experience into the browser. Open DevTools, switch to the "Playwright REPL" tab, and type commands directly. It also includes an action recorder that captures your clicks and form fills as .pw commands.
The extension is standalone (no Node.js daemon needed) — it uses Chrome's chrome.debugger API to drive the inspected page via CDP.
playwright-repl is MIT licensed and works on Linux, macOS, and Windows. Contributions welcome!
2026-02-15 11:51:14
This is a submission for the GitHub Copilot CLI Challenge
Try it yourself:
https://get-outside.pages.dev/
I utilized the .github directory and copilot-instructions.md along with the /agents directory with specific agents such as planning-agent.md, which hands off to either the frontend-agent.md or the backend-agent.md. I also utilized a CLAUDE.md file to have a "master/main scope" for the entire project. This type of flow helped me to not only execute "fast," but also keep a structured flow while developing this MVP.
2026-02-15 11:48:17
🔗 GitHub repo: https://github.com/with-geun/alive-analysis
Over the past year, I’ve been using AI coding agents (Claude Code, Cursor, etc.) heavily for data analysis work.
They’re incredibly helpful — but I kept running into the same problem.
Every analysis was a throwaway conversation.
No structure.
No tracking.
No way to revisit why I reached a conclusion.
A month later, I’d remember what we decided, but not how we got there.
So I built alive-analysis — an open-source workflow kit that adds structure, versioning, and quality checks to AI-assisted analysis.
When you ask an AI to “analyze this data,” you usually get:
In practice, analysis becomes:
I wanted something closer to how real analysis work actually happens — iterative, documented, and revisitable.
alive-analysis structures every analysis using a simple loop:
ASK → LOOK → INVESTIGATE → VOICE → EVOLVE
Define the real question, scope, and success criteria.
Check the data first — quality, segmentation, outliers.
Form hypotheses, test them, and eliminate possibilities.
Document conclusions with confidence levels and audience context.
Capture follow-ups and track impact over time.
Instead of generating answers immediately,
the AI guides you through these stages by asking questions.
That small change alone dramatically improved the rigor of my analyses.
alive-analysis is not a BI tool or dashboard replacement.
You still use:
It simply adds a workflow and documentation layer on top.
After using it for a while, I noticed a few unexpected benefits:
It basically turned AI from an “answer generator” into a thinking partner.
Typical workflow:
Everything lives as markdown in your project, so it becomes a long-term knowledge base instead of lost chat history.
I’d love to hear from people doing real analysis work:
Brutally honest feedback is very welcome 🙏
👉 GitHub: https://github.com/with-geun/alive-analysis
Quick start, examples, and templates are all available in the repo.
If you’ve been using AI for analysis, I’d especially love to know:
👉 What’s the biggest friction you still feel in your workflow?