Every Python beginner hits this wall. You write a simple program that asks the user for a number, try to do math with it, and everything explodes. This isn't a bug or an oversight. It's one of the most deliberately considered design decisions in Python's history — and the story behind it involves a serious security vulnerability, a heated mailing list debate, a formal enhancement proposal, and a set of philosophical principles that define what makes Python Python.
You typed 25, which looks like a number, feels like a number, and should be a number. But Python's input() function handed you back "25" — a string. Not an integer. Not a float. A string. Always.
age = input("Enter your age: ")
next_year = age + 1 # TypeError: can only concatenate str (not "int") to str
What input() Actually Does
Under the hood, Python's input() function performs a straightforward sequence of operations. It writes an optional prompt string to standard output, reads one line of text from sys.stdin, strips the trailing newline character, and returns the result.
That last part is key: it reads from sys.stdin, which is a text stream. sys.stdin is an io.TextIOWrapper object, and you can verify this yourself in the interpreter:
import sys
print(sys.stdin)
# <_io.TextIOWrapper name='<stdin>' mode='r' encoding='utf-8'>
Notice the mode: 'r', for reading text. Not 'rb' for binary. Text streams return strings. That's what text streams do. When the operating system delivers keystrokes from the keyboard to your program, they arrive as a sequence of bytes that Python's text stream decodes into Unicode characters and assembles into a str object.
Fundamentally, input() is doing roughly the same job as this:
import sys
def my_input(prompt=""):
if prompt:
sys.stdout.write(prompt)
sys.stdout.flush()
line = sys.stdin.readline()
if not line:
raise EOFError
if line.endswith('\n'):
line = line[:-1]
return line
There is no step in this process where type inference happens. No moment where Python examines "25" and decides it looks like an integer. The function reads text and returns text, because that's what reading from a text input stream means.
But the deeper question remains: why did Python's designers choose to keep it this way? After all, Python 2 had a version of input() that did try to figure out what you meant. The answer begins with a cautionary tale about what happens when you trust user input.
The Security Disaster: Python 2's input() Was Dangerous
In Python 2, there were two built-in functions for reading user input: raw_input() and input(). They behaved very differently.
raw_input() did exactly what Python 3's input() does today: it read a line of text and returned it as a string.
input(), on the other hand, was equivalent to calling eval(raw_input()). It would take whatever the user typed and evaluate it as a Python expression. Type 42, and you'd get the integer 42. Type 3.14, and you'd get the float 3.14. Convenient? Absolutely. Also a massive security hole.
Consider this Python 2 code for a simple password checker:
# Python 2 -- VULNERABLE CODE
password = get_user_pass("admin")
if password == input("Please enter your password: "):
login()
else:
print "Password is incorrect!"
An attacker who understood how input() worked could simply type password as their password. Not the value of the password variable, but the literal word password. Because input() evaluated the expression, it would resolve password to the variable password in scope, compare the variable to itself, and the condition would evaluate to True. Access granted, no password required.
It gets worse. A malicious user could type something like this at any input() prompt:
__import__('os').system('rm -rf /')
Because input() called eval() on whatever was typed, this would import the os module and execute a system command to delete everything on the filesystem. The user was essentially given access to a full Python interpreter through what appeared to be a simple text prompt.
This was not a theoretical vulnerability. Python 2's input() was flagged in security audits, documented in CTF (Capture the Flag) writeups as a real exploitation vector, and used to build challenges in competitions like picoCTF. The Python 2 documentation itself carried an explicit warning that input() was considered a security risk. An attacker who knew the variable names in scope could bypass authentication entirely without guessing a single character of the real password.
The community consensus was clear and emphatic. Guido van Rossum, Python's creator, stated on the python-3000 mailing list in September 2006 that the old input() function was not to be retained in Python 3000. The eval-based behavior was simply too dangerous to continue offering as a built-in convenience. (Source: PEP 3111, python-3000 mailing list archive, September 2006.)
The Great Renaming: PEP 3111 and the Birth of Modern input()
With the dangerous input() function slated for removal in Python 3, the community faced a new question: what should happen to raw_input()? The original proposal for Python 3.0, outlined in PEP 3100, would have removed both functions from the built-in namespace entirely. If you wanted to read user input, you would need to import sys and call sys.stdin.readline() yourself.
Andre Roberge, a physicist and former university president in Halifax, Nova Scotia, who was known in the Python community for building educational tools like Reeborg's World, recognized the devastating impact this would have on teaching Python to beginners. In September 2006, he initiated a discussion on the edu-sig mailing list titled "The fate of raw_input() in Python 3000," arguing that simple input capabilities were essential for introductory programming education. (Source: edu-sig mailing list, September 2006.)
The conversation moved to the python-3000 mailing list, where Guido van Rossum engaged directly. In September 2006, Guido signaled openness to keeping some form of raw_input() under a new name, while holding firm that the original eval-based input() had to go. This acknowledgment set the stage for what became PEP 3111.
PEP 3111, titled "Simple input built-in in Python 3000" and authored by Roberge, made a compelling case with four specific arguments against forcing beginners to use sys.stdin.readline(): the name was clunky and inelegant; sys and stdin had no meaning for most beginners; dot notation was unmotivated and confusing for novices who might wonder if it worked in any identifier; and there was an asymmetry with output — if print() didn't require sys.stdout.print(), why should input require sys.stdin.readline()? (Source: PEP 3111, peps.python.org.)
The naming debate was spirited. Alternatives included ask(), ask_user(), get_string(), prompt(), read(), user_input(), and get_response(). But the most direct solution was also the most contentious: rename raw_input() to input(), reusing the name of the very function being removed.
Guido initially rejected this idea, concerned that reusing the input name with completely different semantics would confuse developers migrating from Python 2. But by December 2006 his thinking had shifted. In a post to the python-3000 mailing list, he reasoned that the old input() was so rarely used in practice that migration confusion would be minimal, and that the 2to3 conversion tool could handle the mechanical translation automatically. The PEP was formally accepted in December 2006. (Source: python-3000 mailing list, December 2006.)
The decision was made: Python 3's input() would behave exactly like Python 2's raw_input(), always returning a string, with the dangerous eval-based input() eliminated entirely. The 2to3 conversion tool was updated to handle migration automatically: old input() calls became eval(input()) to preserve behavior, and old raw_input() calls became input().
The Philosophical Foundation: Why Strings Are the Right Default
Beyond the security argument, returning a string aligns with several core principles enshrined in the Zen of Python (PEP 20), the set of guiding aphorisms written by Tim Peters in 1999 and standardized as a formal Python Enhancement Proposal in 2004. (Source: PEP 20, peps.python.org.)
"Explicit is better than implicit." This is the second aphorism in the Zen of Python, and it's the single most important principle for understanding why input() returns a string. If input() tried to guess whether you typed an integer, a float, a list, or a string, it would be making implicit decisions about your data. Those decisions might be wrong. When you force the developer to explicitly convert "25" to 25 by writing int(input("Enter your age: ")), there is zero ambiguity about what's happening. The developer has stated their intent, and the code reads clearly.
"In the face of ambiguity, refuse the temptation to guess." What should input() return when the user types 007? Is that the integer 7? The string "007"? What about 1e10 — is that a float or a string? What about True — the boolean, or the string? What about [1, 2, 3] — a list, or a string representation of a list? Python 2's input() tried to answer all of these questions by running eval(). The result was unpredictable, inconsistent, and dangerous. Python 3 refuses to guess. It gives you the raw text, and you decide what to do with it.
"Errors should never pass silently." When you write int(input("Enter a number: ")) and the user types "hello", you get a clear, immediate ValueError. You can catch it, handle it, and tell the user what went wrong. If input() instead silently returned "hello" as a string when it "couldn't figure out" the type, the error would surface later, somewhere unexpected, making debugging harder.
"There should be one — and preferably only one — obvious way to do it." There is one obvious way to read text input in Python 3: input(). There is one obvious way to convert that text to an integer: int(). There is one obvious way to convert it to a float: float(). The pipeline is clear, composable, and each piece has exactly one job.
The Type Safety Argument: Strings as the Universal Input Container
There's a deeper computer science rationale at play. Every data type in Python can be represented as a string, but not every string can be represented as every data type. Strings are the superset, the universal container for textual data.
When a user presses keys on a keyboard, they are producing a sequence of characters. Those characters might look like a number, a date, a JSON object, or a Python expression, but at the moment of entry, they are just characters. Returning them as a string is the only honest thing to do, because a string is the only type that accurately represents what was actually received: text.
This principle extends beyond Python. Every major language follows the same pattern:
// Java: Scanner.nextLine() returns String
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
int number = Integer.parseInt(input);
// JavaScript: prompt() returns a string (or null)
const input = prompt("Enter a number:");
const number = Number(input);
// C: fgets() reads into a char buffer (string)
char buffer[100];
fgets(buffer, sizeof(buffer), stdin);
int number = atoi(buffer);
In every case, the input function reads text and returns text. The conversion to other types is a separate, explicit step. Python didn't invent this pattern; it refined and standardized it.
Practical Patterns: Working With input() the Right Way
Understanding why input() returns a string changes how you approach working with it. Instead of viewing the string return type as an obstacle, treat it as the first step in a validation pipeline.
Basic type conversion:
age = int(input("Enter your age: "))
price = float(input("Enter the price: "))
Safe conversion with error handling:
while True:
try:
age = int(input("Enter your age: "))
if 0 < age < 150:
break
print("Please enter a realistic age.")
except ValueError:
print("That's not a valid number. Try again.")
This pattern is powerful precisely because input() returns a string. You get to validate the raw text before converting, and if conversion fails, you get a clear exception you can handle gracefully.
Using ast.literal_eval() for safe literal parsing:
If you genuinely need to accept different Python literal types from the user, the ast module provides a safe alternative to eval():
import ast
user_data = input("Enter a Python literal: ")
try:
parsed = ast.literal_eval(user_data)
print(f"Got {type(parsed).__name__}: {parsed}")
except (ValueError, SyntaxError):
print("Invalid literal.")
ast.literal_eval() safely evaluates strings containing Python literal structures (strings, numbers, tuples, lists, dicts, booleans, and None) without executing arbitrary code. It was specifically designed as a safe alternative for cases where you need limited type flexibility from string input. Unlike eval(), it cannot import modules, call functions, or access variables — making it the one case where you can accept structured input without opening an execution hole.
Building a type-aware input helper:
def get_input(prompt, cast_to=str, error_msg="Invalid input. Try again."):
"""Read input and convert to the specified type with retry logic."""
while True:
try:
return cast_to(input(prompt))
except (ValueError, TypeError):
print(error_msg)
# Usage
name = get_input("Your name: ") # str by default
age = get_input("Your age: ", int, "Enter a whole number.")
weight = get_input("Your weight in kg: ", float)
This helper function builds on top of input() rather than trying to replace its behavior. The string return type is a feature that enables this kind of composable, reusable pattern.
Using the readline module for richer input:
On Unix-like systems, importing the readline module before your first call to input() unlocks line-editing, history, and tab completion for free — no changes to your input() calls required. The act of importing it hooks into the underlying C readline library, which input() already calls internally on supported platforms.
import readline # Unix/macOS only; silently unavailable on Windows
# Now input() supports arrow-key editing and command history automatically
name = input("Enter your name: ")
The readline module is part of the standard library but is not available on Windows by default. On Windows, a third-party package like pyreadline3 can provide similar functionality. CPython's implementation of input() checks whether sys.stdin is a real terminal (a TTY) before activating readline support — so piped or redirected input still works exactly as expected.
Testing Code That Uses input()
There's a question almost no beginner-level article addresses: how do you write automated tests for a function that calls input()? This is a real problem, and the answer reveals something important about the design.
Because input() reads from sys.stdin, you can replace sys.stdin with any file-like object in your tests. The standard approach uses unittest.mock.patch to substitute a controlled value:
from unittest.mock import patch
def get_age():
while True:
try:
age = int(input("Enter your age: "))
if 0 < age < 150:
return age
print("Please enter a realistic age.")
except ValueError:
print("That's not a valid number.")
# Test with a valid response
with patch('builtins.input', return_value='30'):
result = get_age()
assert result == 30
# Test with an invalid response followed by a valid one
with patch('builtins.input', side_effect=['abc', '25']):
result = get_age()
assert result == 25
The side_effect parameter accepts a list, so you can simulate a realistic sequence of user responses — including bad inputs that trigger retries. This works because patch('builtins.input') replaces the actual input function for the duration of the with block, and your code under test never notices the difference.
The deeper lesson here is architectural: code that calls input() directly is harder to test than code that accepts a value as a parameter. The cleanest pattern is to separate input from logic entirely:
# Harder to test
def run():
age = int(input("Enter your age: "))
print(f"In 10 years you will be {age + 10}.")
# Easier to test
def calculate_future_age(age: int) -> int:
return age + 10
def run():
age = int(input("Enter your age: "))
print(f"In 10 years you will be {calculate_future_age(age)}.")
The business logic lives in calculate_future_age(), which you can test with any value you like. The input() call is confined to the outermost shell of your program, where it belongs. This is exactly the pattern used in professional Python codebases, and it scales all the way up to large applications.
Edge Cases You Haven't Thought About Yet
There are several behaviors of input() that trip up developers even after they understand the type question.
What happens at EOF? If sys.stdin is closed or redirected from a file that reaches its end, input() raises EOFError, not ValueError. If your program can be run non-interactively (piped input, shell scripts), you need to handle this:
try:
name = input("Enter your name: ")
except EOFError:
print("\nNo input received. Exiting.")
raise SystemExit(1)
What about encoding? The encoding used to decode the bytes from sys.stdin is determined by the locale on the operating system, or it can be set explicitly via the PYTHONIOENCODING environment variable. On Windows, this historically defaulted to a legacy codepage encoding that could not represent all Unicode characters. Since Python 3.6, the Windows console uses UTF-8 by default when running in a terminal that supports it, but you may still encounter encoding mismatches when reading from piped input, files, or legacy environments.
import sys
# Check what encoding your stdin is using
print(sys.stdin.encoding) # e.g., 'utf-8' or 'cp1252' on older Windows
What about leading and trailing whitespace? input() strips only the trailing newline. It does not strip leading spaces, trailing spaces, or tabs. A user who accidentally hits the spacebar before pressing Enter will give you " 42", not "42". Whether that becomes a problem depends on your conversion: int(" 42") works fine (Python's int() strips surrounding whitespace), but a string comparison like response == "yes" will fail silently if the user typed " yes".
response = input("Continue? (yes/no): ").strip().lower()
if response == "yes":
continue_operation()
Calling .strip() and .lower() on user input before processing it is a good habit. It costs nothing and prevents a category of bugs that are genuinely hard to spot during manual testing because humans rarely notice the extra space they just typed.
What about multiline input? input() reads one line. Full stop. The newline character is the signal to stop. If you need multiline input, you have to build it yourself:
print("Enter your message (type 'END' on its own line when done):")
lines = []
while True:
line = input()
if line == "END":
break
lines.append(line)
message = "\n".join(lines)
What about type annotations? input()'s return type is str, and type checkers like mypy know this. If you write age = input("Age: ") and then pass age to a function annotated to accept an int, mypy will catch the type mismatch at analysis time — before you run a single line of code. This is another concrete benefit of the explicit string return: it's statically verifiable.
def process_age(age: int) -> str:
return f"You are {age} years old."
raw = input("Enter your age: ") # mypy knows: raw is str
# process_age(raw) # mypy error: Argument 1 to "process_age" has incompatible type "str"; expected "int"
process_age(int(raw)) # correct: explicit conversion satisfies the type checker
The Historical Lens: PEP 3100 and the Bigger Picture
PEP 3111 didn't exist in isolation. It was part of the massive Python 3.0 overhaul documented in PEP 3100 ("Miscellaneous Python 3.0 Plans"), which served as the umbrella proposal for all the breaking changes that would define Python 3. The input() redesign was one of dozens of intentional compatibility breaks made to clean up accumulated design mistakes from Python's first decade.
Other changes in this same wave included making print a function (PEP 3105), making all strings Unicode by default (PEP 3120), and making the / operator perform true division by default for all types, including integers (PEP 238). Each of these changes shared the same DNA: making Python's defaults safer, more predictable, and more explicit.
The input() change was arguably the most straightforward of the lot. Remove the dangerous behavior, keep the safe behavior, give it the simpler name. As documented in the official "What's New in Python 3.0" notes, the old behavior wasn't removed from the language — eval() still exists, and anyone who wants the old behavior can reconstruct it with eval(input()). But it was removed as a default, because the Python core team concluded that the convenience of automatic type inference was not worth the security risk and semantic ambiguity it created. (Source: What's New in Python 3.0, docs.python.org.)
Why This Matters Beyond Beginners
It's tempting to frame the "input returns a string" question as a beginner-only concern. But the design principle behind it — explicit conversion over implicit coercion — is one of the most consequential ideas in Python's architecture.
Consider web development: every HTTP request parameter arrives as a string. Query parameters, form data, URL path segments — all strings. Frameworks like Django and Flask provide explicit mechanisms for parsing and validating these strings into the types your application expects. The pattern is identical to input(). Django's form fields, for instance, run a to_python() method that converts the raw string value to the appropriate Python type — the exact same conceptual step as wrapping input() in int().
Consider file processing: when you read a CSV file, every cell is a string. csv.reader doesn't guess that "42" should be an integer. You convert it explicitly when you need to, because the file format doesn't carry type information.
Consider API development: JSON payloads arrive as text. You explicitly parse them with json.loads(), which returns Python objects according to well-defined mapping rules. Even then, JSON numbers become Python integers or floats, but JSON strings stay strings. No guessing. No coercion. Explicit type mapping, always.
In each case, the boundary between "the outside world" and "your program" is a place where data arrives as text and must be explicitly converted into typed values. Python's input() function teaches this fundamental pattern from the very first interactive program a developer writes. By the time you're building a production web API, the lesson has already been internalized — even if you've forgotten where you learned it.
Key Takeaways
- input() reads from a text stream: sys.stdin is a text stream and text streams return strings. There is no type inference step anywhere in the process.
- Python 2's input() was a security vulnerability: Its eval-based behavior allowed arbitrary code execution from user input and was removed entirely in Python 3. The exploit was documented in real CTF challenges and security audits.
- PEP 3111 formalized the fix: raw_input() was renamed to input() in Python 3, preserving the safe, string-returning behavior and discarding the dangerous one. The PEP was authored by Andre Roberge, a physicist and former university president in Halifax, Nova Scotia, and accepted by Guido van Rossum in December 2006.
- The Zen of Python supports this design: Explicitness over implicitness, refusing to guess in the face of ambiguity, and one obvious way to accomplish each task all point toward the same conclusion.
- Explicit conversion is the correct pattern: int(input()), float(input()), and ast.literal_eval() are not workarounds — they are the intended, idiomatic approach.
- Testing is straightforward: unittest.mock.patch('builtins.input') lets you simulate any sequence of user responses without touching the terminal. Separating input from logic makes code even more testable.
- Edge cases matter in production: Handle EOFError for non-interactive use, check your encoding environment, strip whitespace before comparing strings, and use .strip().lower() as a default habit on user-facing input.
- The pattern scales to professional Python: Web request parameters, CSV cells, and JSON payloads all arrive as strings and require explicit conversion. input() teaches this boundary from day one.
Python's input() returns a string because that's the only safe, honest, and explicit thing it can do. The user typed text. The function returns text. What that text means is your decision, not Python's. The next time you find yourself writing int(input("Enter a number: ")), appreciate the two extra characters for what they are — not boilerplate, but a declaration of intent.
Related PEPs and Sources:
PEP 3111 — Simple input built-in in Python 3000 (Andre Roberge, 2006) — peps.python.org/pep-3111
PEP 3100 — Miscellaneous Python 3.0 Plans (A.M. Kuchling, Brett Cannon) — peps.python.org/pep-3100
PEP 20 — The Zen of Python (Tim Peters, 2004) — peps.python.org/pep-0020
PEP 3105 — Make print a function (Georg Brandl, 2006) — peps.python.org/pep-3105
PEP 3120 — Using UTF-8 as the default source encoding (Martin von Lowis, 2007) — peps.python.org/pep-3120
What's New in Python 3.0 — docs.python.org/3/whatsnew/3.0.html
edu-sig mailing list thread (September 2006) — mail.python.org
python-3000 mailing list, December 2006 rationale — mail.python.org