In which our programs stop talking to themselves and start talking to people.
In This Chapter
Chapter 4: Input, Output, and Formatting — Communicating with the User
In which our programs stop talking to themselves and start talking to people.
Tomás Vieira has a problem. He has written a program — his first real one — that calculates how much of his monthly budget remains after rent. The program compiles. It runs. It produces the correct answer. And it is completely useless.
Why? Because the answer is 328, printed on a blank terminal screen, with no label, no currency symbol, no context whatsoever. When Tomás runs the program and sees 328, he has to remember what that number means. If he shows the output to his roommate, the roommate sees a meaningless integer. If Tomás comes back to the program next week, he might not remember whether that number is dollars, euros, or the number of ramen packets he can afford.
A program that computes the right answer but cannot communicate it clearly is a program that has failed at its fundamental job.
This chapter is about communication. We will learn how to present output in a way that human beings can understand — with labels, alignment, decimal places, and structure. We will learn how to ask users for input and read their responses into variables. We will learn why the apparently simple act of reading a number from the keyboard is full of subtle traps, and how to handle them gracefully. And by the end of the chapter, we will have built the first interactive version of PennyWise — a program that asks Tomás for his expenses, reads his answers, and presents a neatly formatted table that actually makes sense.
This is where programming stops being an abstract exercise and starts being useful.
4.1 Output with Write and WriteLn
You have already met WriteLn in Chapter 2, where we used it to print Hello, Pascal! to the screen. But we treated it as a magic incantation — type this, and words appear. Now we need to understand what Write and WriteLn actually do, and why Pascal gives us two separate procedures for output instead of one.
Before we dive into syntax, a moment of context. In Wirth's original 1970 Pascal, input and output were modeled as sequential files — abstract streams of characters flowing into or out of the program. The standard files Input and Output (corresponding to the keyboard and screen, respectively) were implicit parameters to Read, ReadLn, Write, and WriteLn. This design was deliberately simple: the same procedures that write to the screen will, in Chapter 13, write to files on disk. The consistency is not accidental — it is a design principle. Learn the I/O model once, and you can apply it everywhere.
📊 Theme: Teaching by Design (T1). Pascal's I/O procedures are deliberately minimal. There is no
puts, nocout, noSystem.out.println. There are exactly four procedures for text I/O:Write,WriteLn,Read,ReadLn. This simplicity is intentional. When there is only one way to do something, you learn it thoroughly and use it consistently. Compare this with Python, which hasprint(),sys.stdout.write(),logging, f-strings, andformat()— five different mechanisms for producing output, each with its own conventions.
4.1.1 WriteLn: The Workhorse
WriteLn writes its arguments to the standard output — typically your terminal screen — and then moves the cursor to the beginning of the next line. The "Ln" stands for "line," and it means "end this line when you are done."
program HelloLine;
begin
WriteLn('Hello, world!');
WriteLn('This is a new line.');
end.
Output:
Hello, world!
This is a new line.
Each WriteLn produces one complete line of text. The cursor starts at the left margin after each call. This is the behavior you want most of the time, and it is why WriteLn is the procedure you will use for the vast majority of your output.
4.1.2 Write: Output Without the Newline
Write does exactly what WriteLn does — except it does not move the cursor to the next line. The cursor stays right where it is, immediately after the last character written.
program HelloNoLine;
begin
Write('Hello, ');
Write('world!');
WriteLn; { Now we end the line }
end.
Output:
Hello, world!
Notice that the two Write calls produce their text on the same line. The final WriteLn with no arguments simply ends the line (outputs a newline character without any other text).
Why would you want this? The most common reason is prompting for input. When you ask a user to type something, you want the cursor to remain on the same line as the prompt:
Write('Enter your name: ');
ReadLn(name);
If you used WriteLn here, the cursor would drop to the next line before the user could type, and the visual connection between the prompt and the response would be lost.
💡 Try It Yourself: Write a program with three
Writecalls and one finalWriteLn. Predict what the output will look like before you compile and run it. Were you right?
4.1.3 Multiple Arguments
Both Write and WriteLn accept multiple arguments separated by commas. Pascal will print each argument in sequence, with no spaces between them unless you include spaces yourself:
program MultiArgs;
var
age: Integer;
name: String;
begin
name := 'Tomás';
age := 20;
WriteLn('Name: ', name, ', Age: ', age);
end.
Output:
Name: Tomás, Age: 20
This is important: Pascal does not insert spaces between arguments. If you write WriteLn(a, b) where a is 10 and b is 20, you will get 1020, not 10 20. You must explicitly include the space:
WriteLn(a, ' ', b); { Produces: 10 20 }
4.1.4 Writing Different Data Types
Write and WriteLn can output any of the fundamental types we learned in Chapter 3:
program TypesOutput;
var
i: Integer;
r: Real;
c: Char;
b: Boolean;
s: String;
begin
i := 42;
r := 3.14159;
c := 'A';
b := True;
s := 'Hello';
WriteLn('Integer: ', i);
WriteLn('Real: ', r);
WriteLn('Char: ', c);
WriteLn('Boolean: ', b);
WriteLn('String: ', s);
end.
Output (approximate — the real number format varies):
Integer: 42
Real: 3.1415900000000E+000
Char: A
Boolean: TRUE
String: Hello
Notice something alarming? The real number 3.14159 is displayed as 3.1415900000000E+000 — scientific notation, padded with zeros, and almost unreadable for a human user. This is Pascal's default format for real numbers, and it is terrible for most purposes.
This is exactly why we need formatting.
4.1.5 The Empty WriteLn
A WriteLn with no arguments simply outputs a blank line:
WriteLn('First line');
WriteLn;
WriteLn('Third line, with a blank line above');
This is useful for visual separation in your output. A program that dumps everything on consecutive lines, with no breathing room, is harder to read than one that groups related information with blank lines between sections.
⚠️ Common Pitfall: Do not confuse
WriteLn('')(write an empty string and a newline) withWriteLn(write just a newline). The result is the same, butWriteLnis cleaner and more idiomatic.
4.2 Formatting Output
Unformatted output is readable by compilers. Formatted output is readable by humans. In this section, we learn how to make Pascal display numbers, strings, and columns the way we actually want them to appear.
4.2.1 Field Width: The Single-Colon Specifier
Pascal's formatting system is elegant and simple. After any value in a Write or WriteLn call, you can append a colon followed by an integer to specify the field width — the minimum number of characters the value should occupy:
WriteLn(42:10);
This tells Pascal: "Print the integer 42, but make it take up at least 10 character positions." Since 42 is only 2 characters, Pascal pads it with 8 spaces on the left. The result is a right-justified value in a field of width 10:
42
The general syntax is:
expression : width
where width is a positive integer specifying the minimum number of characters.
Key rules for field width:
- The value is right-justified within the field (padded with spaces on the left).
- If the value requires more characters than the specified width, Pascal ignores the width and prints the full value. The field width is a minimum, not a maximum — it never truncates.
- Strings, integers, booleans, and characters all accept the
:widthspecifier.
program FieldWidths;
begin
WriteLn('|', 42:10, '|');
WriteLn('|', 12345:10, '|');
WriteLn('|', 'Hello':10, '|');
WriteLn('|', True:10, '|');
end.
Output:
| 42|
| 12345|
| Hello|
| TRUE|
The pipe characters (|) are just visual markers to show where the field begins and ends. Notice how every value is pushed to the right side of a 10-character field.
4.2.2 Decimal Places: The Double-Colon Specifier
For Real numbers (and Double, Extended, and other floating-point types), you can add a second colon and integer to specify how many decimal places to display:
WriteLn(3.14159:8:2);
This means: "Print 3.14159 in a field of width 8, with exactly 2 decimal places." The result:
3.14
The general syntax for real numbers is:
expression : width : decimals
This is transformative. Without the decimal specifier, Pascal uses scientific notation. With it, Pascal uses fixed-point notation — the format humans actually read:
program DecimalDemo;
var
price: Real;
begin
price := 49.99;
WriteLn('Unformatted: ', price);
WriteLn('Formatted: ', price:10:2);
WriteLn('Tight: ', price:0:2);
end.
Output:
Unformatted: 4.9990000000000E+001
Formatted: 49.99
Tight: 49.99
Notice the price:0:2 on the last line. A width of 0 tells Pascal to use the minimum number of characters needed — no padding. This is the most common way to format a price inline within a sentence.
✅ Best Practice: For currency values, always use
:0:2for inline display or a specific width like:10:2for tabular display. Never display a currency value in unformatted scientific notation.
4.2.3 Building Formatted Tables
Field-width specifiers are the key to building aligned columns. If every value in a column uses the same field width, the columns will line up perfectly:
program SimpleTable;
begin
WriteLn('Item':20, 'Qty':8, 'Price':10, 'Total':10);
WriteLn('--------------------', '--------', '----------', '----------');
WriteLn('Coffee Beans':20, 2:8, 12.50:10:2, 25.00:10:2);
WriteLn('Milk (gallon)':20, 1:8, 4.29:10:2, 4.29:10:2);
WriteLn('Sugar (5 lb)':20, 1:8, 6.99:10:2, 6.99:10:2);
WriteLn('--------------------', '--------', '----------', '----------');
WriteLn('Total':20, '':8, '':10, 35.28:10:2);
end.
Output:
Item Qty Price Total
---------------------------- --------------------
Coffee Beans 2 12.50 25.00
Milk (gallon) 1 4.29 4.29
Sugar (5 lb) 1 6.99 6.99
---------------------------- --------------------
Total 35.28
This is one of Pascal's quietly brilliant features. Where C requires the complex printf format strings (%10.2f) and Python needs f-strings or format specifications, Pascal's colon syntax reads naturally within the WriteLn call itself. You can see the structure of the table in the code.
🔗 Transfer Note: In C, you would write
printf("%-20s%8d%10.2f%10.2f\n", item, qty, price, total). In Python 3:print(f"{item:<20}{qty:>8}{price:>10.2f}{total:>10.2f}"). Pascal's approach is arguably the most readable for simple cases, because the format specifiers sit right next to the values they format.
4.2.4 Rosa's Expense Table
Let us see how Rosa Martinelli uses formatting to make her freelance expense data readable. Rosa tracks business expenses — design software subscriptions, printing costs, client meeting lunches — and she wants a quick report she can show her accountant.
Here is Rosa's first attempt, without formatting:
WriteLn('Adobe Creative Cloud', 54.99);
WriteLn('Printer Paper (500 sheets)', 12.49);
WriteLn('Client Lunch - Cafe Roma', 38.50);
WriteLn('USB Drive 64GB', 9.99);
The output is a mess — numbers jammed against text, no decimal alignment, no visual structure. Now here is her second attempt, with field-width formatting:
WriteLn('Expense':30, 'Amount':10);
WriteLn('------------------------------', '----------');
WriteLn('Adobe Creative Cloud':30, 54.99:10:2);
WriteLn('Printer Paper (500 sheets)':30, 12.49:10:2);
WriteLn('Client Lunch - Cafe Roma':30, 38.50:10:2);
WriteLn('USB Drive 64GB':30, 9.99:10:2);
WriteLn('------------------------------', '----------');
WriteLn('TOTAL':30, 115.97:10:2);
Output:
Expense Amount
----------------------------------------
Adobe Creative Cloud 54.99
Printer Paper (500 sheets) 12.49
Client Lunch - Cafe Roma 38.50
USB Drive 64GB 9.99
----------------------------------------
TOTAL 115.97
The dollar amounts are perfectly aligned on the decimal point. The column header "Amount" sits above the numbers. The total visually belongs in the same column as the individual amounts. Rosa can hand this to her accountant and it requires no explanation.
This is the power of formatted output: the same data, transformed from chaos into clarity by nothing more than field-width specifiers.
✅ Best Practice: When designing a formatted table, decide your column widths before writing the code. Write down the maximum expected length of each column's data, add a few characters of padding, and use that as your field width. Consistency across all rows is what creates visual alignment.
4.2.5 Left-Justifying Strings
You may have noticed that strings, like numbers, are right-justified by default. This means a short string in a wide field has leading spaces — fine for numbers, but odd for text labels in the first column of a table.
Standard Pascal does not have a built-in left-justification specifier. However, there are several practical approaches:
Approach 1: Pad the string yourself. The simplest method is to append spaces to your string to fill the field, then use a field width that matches:
WriteLn('Coffee Beans ', 2:8, 12.50:10:2);
This is clumsy but works for hardcoded text.
Approach 2: Use a helper function. Write a function that pads a string on the right to a given width. We will learn to write functions in Chapter 7, but here is a preview of what it looks like:
function PadRight(s: String; width: Integer): String;
begin
while Length(s) < width do
s := s + ' ';
PadRight := s;
end;
Then: Write(PadRight('Coffee Beans', 20)).
Approach 3: Use Free Pascal's Format function. Free Pascal provides a Format function in the SysUtils unit that supports left-justification with a negative width:
uses SysUtils;
// ...
WriteLn(Format('%-20s%8d%10.2f', ['Coffee Beans', 2, 12.50]));
We mention this for completeness, but in this chapter we will stick with the native Pascal formatting syntax, which handles most cases well.
4.2.6 Formatting Booleans and Characters
Boolean and character types also accept the field-width specifier:
WriteLn(True:8); { Outputs: " TRUE" }
WriteLn(False:8); { Outputs: " FALSE" }
WriteLn('A':5); { Outputs: " A" }
Booleans are displayed as TRUE or FALSE (in uppercase). Characters are displayed as themselves. Both are right-justified in the specified field width.
💡 Try It Yourself: Create a multiplication table (e.g., 1 through 5) where every number is formatted in a field of width 6. The table should have neat, aligned columns.
4.3 Input with Read and ReadLn
Output is half the conversation. The other half is input — getting information from the user. Pascal provides two procedures for reading from the keyboard: Read and ReadLn. Understanding the difference between them is one of the key skills of this chapter.
4.3.1 ReadLn: Reading a Complete Line
ReadLn reads input from the keyboard and stores it in one or more variables. After reading, it discards everything remaining on the line, including the newline character that the user typed by pressing Enter:
program ReadDemo;
var
name: String;
begin
Write('What is your name? ');
ReadLn(name);
WriteLn('Hello, ', name, '!');
end.
When the user runs this program, they see:
What is your name? Tomás
Hello, Tomás!
The ReadLn(name) call waits for the user to type something and press Enter. Whatever they typed (before the Enter key) is stored in the name variable. The newline from pressing Enter is consumed and discarded.
ReadLn can read into variables of different types. Pascal automatically converts the text the user types into the appropriate type:
var
age: Integer;
height: Real;
begin
Write('Enter your age: ');
ReadLn(age); { User types "20", stored as Integer 20 }
Write('Enter your height in meters: ');
ReadLn(height); { User types "1.75", stored as Real 1.75 }
end.
4.3.2 Reading Multiple Values
Both Read and ReadLn can accept multiple variable arguments. When reading multiple values, Pascal expects them to be separated by whitespace (spaces, tabs, or newlines):
var
x, y: Integer;
begin
Write('Enter two numbers separated by a space: ');
ReadLn(x, y);
WriteLn('Sum = ', x + y);
end.
If the user types 10 25 and presses Enter, x gets 10 and y gets 25. The space between them acts as a delimiter.
This works, but it places an invisible burden on the user: they must know to separate values with spaces. For interactive programs, we generally prefer to read one value per ReadLn call, with a clear prompt for each:
Write('Enter first number: ');
ReadLn(x);
Write('Enter second number: ');
ReadLn(y);
This is longer but much more user-friendly.
4.3.3 Tomás Enters His Daily Expenses
Let us watch Tomás use input and output together for the first time. He wants a program that asks how much he spent on lunch and tells him how much of his daily budget remains:
program DailyBudget;
var
budget: Real;
spent: Real;
remaining: Real;
begin
budget := 25.00; { Tomás's daily spending budget }
WriteLn('=== Daily Budget Tracker ===');
WriteLn;
WriteLn('Your daily budget: $', budget:0:2);
WriteLn;
Write('How much did you spend on lunch? $');
ReadLn(spent);
remaining := budget - spent;
WriteLn;
WriteLn('You spent: $', spent:0:2);
WriteLn('Remaining: $', remaining:0:2);
if remaining < 0 then
WriteLn('WARNING: You have exceeded your daily budget!')
else
WriteLn('You are within budget. Keep it up!');
end.
A sample session:
=== Daily Budget Tracker ===
Your daily budget: $25.00
How much did you spend on lunch? $12.50
You spent: $12.50
Remaining: $12.50
You are within budget. Keep it up!
This is a tiny program — barely twenty lines — but it is the first time Tomás has built something that responds to him. The program asks a question, listens to the answer, computes a result, and communicates it with context. That is a qualitative leap from Chapter 3's programs, which could only display predetermined values.
(We have used an if statement here as a preview — Chapter 5 will explain it fully. For now, it reads like English: "if remaining is less than zero, then print a warning.")
4.3.4 Coming From Other Languages
If you have programmed before, here is how Pascal's input model compares:
Coming From Python: Python's
input()always returns a string, which you must explicitly convert:age = int(input("Age: ")). Pascal'sReadLn(age)does the conversion automatically — but crashes if the input is not a valid integer. The Python approach is more forgiving; the Pascal approach is more explicit about what it expects. Both have tradeoffs. TheValprocedure (Section 4.5) gives you Python-style safety with Pascal-style typing.Coming From C: C's
scanf("%d", &n)requires format strings and the address-of operator (&) — two concepts that are easy to get wrong and produce undefined behavior when misused. Pascal'sReadLn(n)requires neither. The type ofndetermines how the input is parsed. No format strings, no pointer arithmetic, no buffer overflows.Coming From Java: Java's
Scannerclass requires creating an object, choosing the right method (nextInt(),nextDouble(),nextLine()), and handling exceptions. Pascal'sReadLnis a single procedure that works with any type. The simplicity is striking — and deliberate.
4.3.5 The Prompt Pattern
The most important I/O pattern in Pascal — indeed, in almost any language — is the prompt-read pattern:
Write('Enter something: '); { Prompt — no newline }
ReadLn(variable); { Read — consumes the line }
Note the use of Write (not WriteLn) for the prompt. This keeps the cursor on the same line as the prompt text, so the user types immediately after the colon and space. This is the standard way to interact with a user in a console program, and you will use it hundreds of times.
✅ Best Practice: Always use
Writefor prompts (notWriteLn), always end the prompt with a space or colon-space, and always useReadLn(notRead) for interactive input.
4.3.6 Reading Different Types
Pascal reads different types differently:
| Type | What the user types | What Pascal stores |
|---|---|---|
Integer |
42 |
The integer value 42 |
Real |
3.14 |
The floating-point value 3.14 |
Char |
A |
The character 'A' (one character only) |
String |
Hello World |
The string "Hello World" (entire line for ReadLn) |
Boolean |
— | Cannot read Booleans directly |
Two important notes:
-
Reading a
Char:ReadLn(c)wherecis aCharreads only the first character of whatever the user types. The rest of the line is discarded (because it isReadLn, notRead). -
Reading a
String:ReadLn(s)reads everything the user types until they press Enter, including spaces. This is different from many languages where "read a word" means "read until the first space."
4.3.7 What Happens When Types Do Not Match
What happens when you ask for an integer and the user types banana? Pascal crashes — or more precisely, the program terminates with a runtime error:
Runtime error 106 at $00401234
Error 106 is "Invalid numeric format." Pascal tried to convert the text banana into an integer, failed, and gave up. This is not graceful, and in Section 4.5 we will learn how to handle this properly. For now, understand that Pascal trusts the user to enter valid data when you use a simple ReadLn with a numeric variable. If that trust is misplaced, the program dies.
This is actually an honest design. Pascal does not silently return zero or ignore the error. It tells you, loudly, that something went wrong. In Wirth's philosophy, a program that appears to work but has silently swallowed an error is far more dangerous than one that crashes explicitly.
4.4 The Input Buffer Problem
This section explains one of the most confusing aspects of console I/O in Pascal — and, frankly, in most languages. Understanding the input buffer will save you hours of debugging.
4.4.1 How the Input Buffer Works
When the user types at the keyboard and presses Enter, the characters they typed (plus the newline character from pressing Enter) are placed into a buffer — a temporary storage area in memory. Pascal's Read and ReadLn procedures do not actually read from the keyboard; they read from this buffer.
Think of it like a conveyor belt at a grocery checkout. The user places items (characters) on the belt and presses Enter (which adds a "belt ends here" marker). Pascal's reading procedures then pick items off the belt one at a time.
4.4.2 Read vs. ReadLn: The Critical Difference
Here is where the distinction between Read and ReadLn becomes crucial:
ReadLnreads the requested values from the buffer and then discards everything remaining on the line, including the newline character. It clears the belt.Readreads the requested values from the buffer and stops. It does not discard the rest of the line. Whatever is left — including the newline character — stays in the buffer for the next read operation.
This difference seems subtle, but it causes one of the most common bugs in Pascal programs:
program BufferProblem;
var
a, b: Integer;
begin
Write('Enter first number: ');
Read(a); { Reads the number, leaves \n in buffer }
Write('Enter second number: ');
Read(b); { Might read the \n, or skip past it }
WriteLn('Sum = ', a + b);
end.
If the user types 10 and presses Enter, Read(a) consumes the characters 1 and 0 to form the integer 10. But the newline character from pressing Enter is still in the buffer. What happens next depends on the type of the next variable being read. For integers, Pascal will skip whitespace (including the lingering newline) and wait for more input, so the program appears to work. But consider this version with characters:
program BufferBug;
var
c1, c2: Char;
begin
Write('Enter first character: ');
Read(c1); { Reads 'A', leaves \n in buffer }
Write('Enter second character: ');
Read(c2); { Reads the \n from the buffer! }
WriteLn('You entered: ', c1, ' and ', c2);
end.
The user types A and presses Enter, expecting to be asked for a second character. Instead, the program immediately finishes, because Read(c2) consumed the newline character that was left in the buffer. The user never got a chance to type the second character.
This is the input buffer problem, and it has confused generations of Pascal students.
4.4.3 The Solution: Prefer ReadLn
The solution is straightforward: use ReadLn for interactive programs, not Read. The ReadLn procedure always cleans up after itself by discarding the rest of the line:
program BufferFixed;
var
c1, c2: Char;
begin
Write('Enter first character: ');
ReadLn(c1); { Reads 'A', discards \n }
Write('Enter second character: ');
ReadLn(c2); { Reads 'B', discards \n }
WriteLn('You entered: ', c1, ' and ', c2);
end.
Now the program works correctly. After reading c1, ReadLn clears the buffer. The second ReadLn starts with a fresh buffer and waits for the user to type.
⚠️ Common Pitfall: If you see a program that "skips" an input prompt — it asks a question but never lets you answer — the cause is almost always a leftover newline in the input buffer from a previous
Readcall. Switch toReadLn.
4.4.4 When to Use Read (Rare Cases)
Read (without the "Ln") is useful when reading multiple values from a single line of input, typically in batch processing or when reading from files. For example:
{ Reading three numbers from one line of input }
Write('Enter three numbers: ');
Read(a, b, c);
ReadLn; { Clean up the rest of the line }
Notice the bare ReadLn at the end. This is a common pattern: use Read to consume the values, then ReadLn to discard the trailing newline. But in practice, for interactive programs, you almost always want one prompt and one ReadLn per value.
✅ Best Practice: In interactive console programs, always use
ReadLn. ReserveReadfor file I/O and batch processing, where you control the input format. When you must useReadin an interactive context, follow it with a bareReadLnto clear the buffer.
4.5 Input Validation Basics
In the real world, users make mistakes. They type letters when you expect numbers. They press Enter without typing anything. They enter negative values when you need positive ones. A professional program anticipates these mistakes and handles them gracefully instead of crashing.
4.5.1 The Runtime Error Problem
As we saw in Section 4.3.5, reading a non-numeric value into a numeric variable causes a runtime error. For a quick prototype or homework assignment, this might be acceptable. For any program that a real person will use — even Tomás tracking his lunch expenses — it is not.
Pascal provides two mechanisms for handling bad input:
- The
Valprocedure (the focus of this section) - The
{$I-}compiler directive withIOResult(a preview — we will explore this fully in Chapter 13)
4.5.2 The Val Procedure
The Val procedure converts a string to a number, and tells you whether the conversion succeeded. Its syntax is:
Val(sourceString, numericVariable, errorCode);
sourceStringis the text to convert (typeString).numericVariableis the variable to store the result (typeInteger,Real, etc.).errorCodeis anIntegervariable that receives the position of the first invalid character, or0if the conversion succeeded.
Here is the pattern:
program SafeInput;
var
input: String;
amount: Real;
code: Integer;
begin
Write('Enter an amount: ');
ReadLn(input); { Read as a string — this never fails }
Val(input, amount, code); { Try to convert to a number }
if code = 0 then
WriteLn('You entered: $', amount:0:2)
else
WriteLn('Error: "', input, '" is not a valid number.');
end.
The key insight is that we read the input as a string first. Reading a string always succeeds — whatever the user types, it is a valid string. Then we use Val to attempt the conversion, checking the result before proceeding.
This is a fundamental defensive programming pattern: separate reading from parsing.
💡 Try It Yourself: Modify the program above to keep asking for input until the user enters a valid number. (Hint: you will need a loop, which we cover in Chapter 6. For now, you can use a
repeat..untilloop — the syntax isrepeat ... until code = 0;.)
4.5.3 Understanding the Error Code
The code parameter from Val is not just a boolean success/failure indicator. It tells you where in the string the conversion failed:
Val('12.3abc', amount, code);
{ code = 5, because position 5 ('a') is the first invalid character }
Val('abc', amount, code);
{ code = 1, because position 1 ('a') is immediately invalid }
Val('42', intVar, code);
{ code = 0, conversion succeeded }
This can be useful for producing specific error messages, but in practice, most programs simply check whether code is zero (success) or non-zero (failure).
4.5.4 Validating Ranges and Conditions
Even when the user enters a valid number, it might not be a sensible number. An expense amount should not be negative. A menu choice of 7 is invalid when the menu only has 4 options. Validation means checking not just that the input is the right type, but that it falls within the expected range:
Val(input, amount, code);
if code <> 0 then
WriteLn('Error: not a valid number.')
else if amount < 0 then
WriteLn('Error: amount cannot be negative.')
else if amount > 100000 then
WriteLn('Error: amount seems unreasonably large.')
else
WriteLn('Accepted: $', amount:0:2);
We will explore if-then-else in depth in Chapter 5. For now, the pattern should be readable: check for errors in order from most severe to least severe, and only accept the value if it passes all checks.
4.5.5 The {$I-} Directive (Preview)
Pascal also supports a lower-level mechanism for handling I/O errors. The compiler directive {$I-} turns off automatic I/O error checking. When it is off, I/O errors do not crash the program — instead, they set an internal error code that you can check with the IOResult function:
{$I-}
ReadLn(amount);
{$I+}
if IOResult <> 0 then
WriteLn('Invalid input!');
This approach is powerful but less beginner-friendly than Val. We will cover it properly in Chapter 13 when we discuss file I/O. For now, stick with the string-then-Val pattern.
4.6 Building Text-Based User Interfaces
A "text-based user interface" may sound like an oxymoron in an era of touch screens, but console programs are still everywhere: server administration, data processing pipelines, developer tools, and — importantly for us — learning. A well-designed text UI is a pleasure to use. A badly designed one is a nightmare.
4.6.1 Designing a Menu
The most basic interactive pattern is a menu: display numbered options, read the user's choice, and act on it. Here is the visual design:
=================================
PENNYWISE EXPENSE TRACKER
=================================
1. Add an expense
2. View expense summary
3. Exit
Enter your choice (1-3):
The code to display this (without the actual logic — that comes in Chapter 5 when we learn case statements) looks like:
WriteLn('=================================');
WriteLn(' PENNYWISE EXPENSE TRACKER ');
WriteLn('=================================');
WriteLn;
WriteLn(' 1. Add an expense');
WriteLn(' 2. View expense summary');
WriteLn(' 3. Exit');
WriteLn;
Write('Enter your choice (1-3): ');
ReadLn(choice);
Notice the details: the title is centered within the border, there are blank lines for breathing room, options are indented and numbered, and the prompt uses Write (not WriteLn) so the cursor stays on the prompt line.
4.6.2 Building Tables with Box Characters
For more sophisticated tables, you can use box-drawing characters or simple ASCII art:
WriteLn('+----------------------+----------+');
WriteLn('| Category | Amount |');
WriteLn('+----------------------+----------+');
WriteLn('| Food | $45.00 |');
WriteLn('| Transportation | $22.50 |');
WriteLn('| Entertainment | $15.75 |');
WriteLn('+----------------------+----------+');
WriteLn('| TOTAL | $83.25 |');
WriteLn('+----------------------+----------+');
This creates a visual frame that makes the data easier to scan. The alignment comes from careful use of field widths and strategic placement of pipe characters.
To build such a table programmatically (with data in variables, not hardcoded), you combine Write with field-width formatting:
procedure PrintRow(category: String; amount: Real);
begin
Write('| ');
Write(category);
{ Pad category to 20 characters }
Write(' ':20 - Length(category));
Write(' | ');
Write('$', amount:7:2);
WriteLn(' |');
end;
We are using a few tricks here. The expression ' ':20 - Length(category) prints a single space character in a field of width 20 - Length(category), which effectively pads the category name to 20 characters. This technique for left-justification using a space character with a calculated field width is a classic Pascal idiom.
🔗 Cross-Reference: We will revisit this table-building technique in Chapter 7 when we can encapsulate it in procedures, and again in Chapter 9 when we can store multiple rows in arrays.
4.6.3 Horizontal Rules and Separators
A simple but effective technique for visual structure is the horizontal rule — a line of repeated characters:
{ A simple separator }
WriteLn('----------------------------------------');
{ A double-line separator }
WriteLn('========================================');
{ A separator built from a counted string }
var
i: Integer;
begin
for i := 1 to 40 do
Write('-');
WriteLn;
end;
We will learn the for loop formally in Chapter 6. For now, note that it simply repeats the Write('-') statement 40 times.
4.6.4 Designing for Readability
A few principles make the difference between a console UI that feels professional and one that feels like a homework assignment:
Principle 1: White space is not wasted space. Blank lines between sections help the eye group related information. A menu crammed into consecutive lines, with no breathing room, is harder to scan than one with blank lines separating the header, the options, and the prompt.
Principle 2: Consistency in alignment. If your first menu option starts with two spaces of indentation, every option should start with two spaces. If your first data column is 10 characters wide, every row should use a 10-character field width. Inconsistency creates visual noise that makes users work harder to find the information they need.
Principle 3: Labels before numbers. A number without a label is meaningless. $42.50` could be a balance, a payment, a refund, or a rounding error. `Remaining balance: $42.50 is unambiguous. Always label your output.
Principle 4: Confirm user actions. After the user enters data, echo it back. "You entered: $12.50 for Food." This gives the user confidence that the program understood them correctly, and it catches typos immediately rather than letting them propagate silently through calculations.
These are not Pascal-specific principles — they apply to any language, any interface. But Pascal's structured I/O makes them easy to implement, because you can see the formatting right there in the code.
4.6.5 Clearing the Screen (Platform Note)
Many tutorials suggest "clearing the screen" as part of a console UI. In Pascal, this is platform-dependent:
{ Windows }
{ uses Windows; }
{ ClrScr is available in the CRT unit }
uses CRT;
begin
ClrScr; { Clears the terminal screen }
end.
The CRT unit, available in Free Pascal, provides ClrScr along with other console manipulation procedures like GotoXY (position the cursor), TextColor (change text color), and Delay (pause execution). These are useful for building interactive console applications, but they are not part of standard Pascal — they are Free Pascal extensions.
📊 Theme: Living Language (T3). The
CRTunit is a good example of how Free Pascal extends standard Pascal for practical purposes. It originated in Borland's Turbo Pascal in the 1980s and has been faithfully maintained in Free Pascal. Features that real programmers need do not disappear — they evolve.
4.7 Practical Considerations
Before we move to the project checkpoint, let us address several practical issues that affect how your I/O code behaves in the real world.
4.7.1 The Str Procedure: Formatting Into Strings
The Val procedure converts strings to numbers. Its counterpart, Str, converts numbers to strings with formatting:
var
amount: Real;
text: String;
begin
amount := 42.99;
Str(amount:0:2, text);
{ text now contains the string '42.99' }
WriteLn('The amount is $' + text);
end.
The syntax is Str(expression:width:decimals, stringVariable). The format specifiers work exactly as they do in Write and WriteLn. This is useful when you need to build a formatted string for later use — for example, constructing a message that includes a formatted number, or preparing data for file output.
We mention Str here for completeness; you will use it more frequently in later chapters when you work with string manipulation (Chapter 10) and file output (Chapter 13).
4.7.2 Cross-Platform Line Endings
Different operating systems use different characters to represent the end of a line:
| OS | Line ending | Characters |
|---|---|---|
| Windows | CR+LF | #13#10 |
| Linux/macOS | LF | #10 |
| Old Mac (pre-OS X) | CR | #13 |
Free Pascal handles this transparently when reading from the console — ReadLn works correctly on all platforms. But you should be aware of this when reading from files (Chapter 13) or when your program produces text files that will be opened on a different operating system.
4.7.3 Console Encoding and Unicode
If Tomás wants to display his name with the accent mark (Tomás, not Tomas), the program needs to handle characters outside the basic ASCII range. Free Pascal supports UTF-8 strings in its {$mode objfpc}` and `{$mode delphi} modes, and modern terminals generally display UTF-8 correctly.
For most of this textbook, we will stay within ASCII for simplicity. But know that Free Pascal fully supports Unicode, and if you need accented characters, emoji, or non-Latin scripts, the capability is there.
4.7.4 Terminal Colors with the CRT Unit
The CRT unit provides colored text output:
uses CRT;
begin
TextColor(Red);
WriteLn('ERROR: Invalid amount!');
TextColor(Green);
WriteLn('SUCCESS: Expense recorded.');
TextColor(White); { Reset to default }
end.
Colors make error messages stand out and success confirmations reassuring. Use them sparingly — a rainbow of colors is harder to read than black and white.
📊 Theme: Native Compiled (T4). When you compile this program, Free Pascal produces a standalone native executable that interacts directly with the terminal. There is no runtime interpreter, no virtual machine, no garbage collector running in the background. The
WriteandWriteLncalls compile down to efficient system calls. This is why Pascal programs start instantly and run fast — even a simple I/O program demonstrates the native compilation advantage.
4.7.5 Output Buffering
On some systems, Write output may be buffered — accumulated in memory and only flushed to the screen when a full line is completed (by WriteLn) or when the buffer is full. This can cause prompts to be invisible: the user is waiting for a prompt that the operating system has not yet displayed.
In practice, Free Pascal flushes standard output before every ReadLn, so the prompt-read pattern works correctly. But if you ever encounter a situation where a Write prompt does not appear, you can force a flush:
Write('Enter value: ');
Flush(Output); { Force the output buffer to the screen }
ReadLn(value);
4.7.6 Comparing Pascal I/O with Other Languages
Let us put Pascal's I/O in perspective with a comparison:
| Task | Pascal | C | Python |
|---|---|---|---|
| Print with newline | WriteLn('Hi'); |
printf("Hi\n"); |
print("Hi") |
| Print without newline | Write('Hi'); |
printf("Hi"); |
print("Hi", end="") |
| Read integer | ReadLn(n); |
scanf("%d", &n); |
n = int(input()) |
| Format number | Write(x:8:2); |
printf("%8.2f", x); |
print(f"{x:8.2f}") |
| Read string | ReadLn(s); |
fgets(s, sizeof(s), stdin); |
s = input() |
Pascal's I/O is more verbose than Python's but far safer than C's (no format-string vulnerabilities, no pointer arithmetic). The explicit separation of Write and WriteLn is clearer than C's \n convention. And Pascal's format specifiers — the colon syntax — are syntactically simpler than either C's % codes or Python's mini-language.
🔗 Transfer Note: If you later learn C, you will find that
printfandscanfare more powerful but also more dangerous. The%sformat specifier inscanfdoes not check buffer bounds and is a classic source of security vulnerabilities. Pascal's approach — reading into typed variables with length limits on strings — is inherently safer.
4.8 Project Checkpoint: PennyWise v0.3 — Interactive Expense Entry
It is time to put everything together. In Chapter 3, we defined variables for an expense amount, category, and description, and displayed them. Now we will make PennyWise interactive: the program asks Tomás for his expenses and displays them in a formatted table.
4.8.1 What We Are Building
PennyWise v0.3 is a simple interactive program that:
- Displays a welcome header
- Asks the user for an expense amount (with validation)
- Asks for a category name
- Asks for a description
- Repeats for a fixed number of expenses (three, for now — we do not have loops yet)
- Displays all expenses in a formatted table with headers
- Calculates and displays the total
4.8.2 The Complete Program
Here is the complete PennyWise v0.3 checkpoint. Study it carefully — it uses every technique we have covered in this chapter:
program PennyWise;
{ PennyWise v0.3 - Interactive Expense Entry }
{ Chapter 4 checkpoint: Input, output, and formatting }
var
{ Expense 1 }
amount1: Real;
category1: String;
desc1: String;
{ Expense 2 }
amount2: Real;
category2: String;
desc2: String;
{ Expense 3 }
amount3: Real;
category3: String;
desc3: String;
{ Totals }
total: Real;
{ Input validation }
inputStr: String;
valCode: Integer;
begin
{ === Welcome Header === }
WriteLn('================================================');
WriteLn(' PENNYWISE - Personal Expense Tracker ');
WriteLn(' Version 0.3 ');
WriteLn('================================================');
WriteLn;
WriteLn('Enter 3 expenses to get started.');
WriteLn;
{ === Expense 1 === }
WriteLn('--- Expense #1 ---');
repeat
Write(' Amount ($): ');
ReadLn(inputStr);
Val(inputStr, amount1, valCode);
if valCode <> 0 then
WriteLn(' Please enter a valid number.');
until valCode = 0;
Write(' Category: ');
ReadLn(category1);
Write(' Description: ');
ReadLn(desc1);
WriteLn;
{ === Expense 2 === }
WriteLn('--- Expense #2 ---');
repeat
Write(' Amount ($): ');
ReadLn(inputStr);
Val(inputStr, amount2, valCode);
if valCode <> 0 then
WriteLn(' Please enter a valid number.');
until valCode = 0;
Write(' Category: ');
ReadLn(category2);
Write(' Description: ');
ReadLn(desc2);
WriteLn;
{ === Expense 3 === }
WriteLn('--- Expense #3 ---');
repeat
Write(' Amount ($): ');
ReadLn(inputStr);
Val(inputStr, amount3, valCode);
if valCode <> 0 then
WriteLn(' Please enter a valid number.');
until valCode = 0;
Write(' Category: ');
ReadLn(category3);
Write(' Description: ');
ReadLn(desc3);
WriteLn;
{ === Calculate total === }
total := amount1 + amount2 + amount3;
{ === Formatted Output === }
WriteLn('================================================');
WriteLn(' YOUR EXPENSE SUMMARY ');
WriteLn('================================================');
WriteLn;
WriteLn(' Amount':10, 'Category':14, 'Description':22);
WriteLn(' --------', ' ------------', ' --------------------');
WriteLn(amount1:10:2, category1:14, desc1:22);
WriteLn(amount2:10:2, category2:14, desc2:22);
WriteLn(amount3:10:2, category3:14, desc3:22);
WriteLn(' --------', ' ------------', ' --------------------');
WriteLn(total:10:2, 'TOTAL':14);
WriteLn;
WriteLn('================================================');
WriteLn('Thank you for using PennyWise!');
end.
4.8.3 Walking Through the Code
Let us trace through the key decisions in this program:
Input validation. We use the repeat..until loop (a preview from Chapter 6) with Val to ensure the amount is a valid number. If the user types banana, they see "Please enter a valid number" and are asked again. The program never crashes.
Prompt formatting. All prompts are aligned: Amount ($):, Category:, and Description: each start at the same column because we use consistent indentation. The colons are aligned by padding the label text. Small details like this make a program feel polished.
The formatted table. The summary uses field widths of 10, 14, and 22 for the three columns. The numbers are formatted with :10:2 — two decimal places in a 10-character field. The category and description strings are right-justified in their fields. The separator lines visually mark the top and bottom of the data section.
The total. We calculate total as a simple sum and display it on a final row, aligned with the amount column.
4.8.4 A Sample Session
Here is what Tomás sees when he runs the program:
================================================
PENNYWISE - Personal Expense Tracker
Version 0.3
================================================
Enter 3 expenses to get started.
--- Expense #1 ---
Amount ($): 12.50
Category: Food
Description: Lunch at dining hall
--- Expense #2 ---
Amount ($): oops
Please enter a valid number.
Amount ($): 35.00
Category: Books
Description: Used chemistry textbook
--- Expense #3 ---
Amount ($): 8.75
Category: Food
Description: Coffee and snacks
================================================
YOUR EXPENSE SUMMARY
================================================
Amount Category Description
-------- ------------ --------------------
12.50 Food Lunch at dining hall
35.00 Books Used chemistry textbook
8.75 Food Coffee and snacks
-------- ------------ --------------------
56.25 TOTAL
================================================
Thank you for using PennyWise!
Notice how the validation caught Tomás's mistake ("oops") and asked again. Notice how the columns are aligned. Notice how the total is calculated correctly. This is a simple program, but it is a usable program.
4.8.5 What Is Missing (And What Comes Next)
PennyWise v0.3 has obvious limitations:
- The number of expenses is hardcoded to three. We need loops (Chapter 6).
- The expense entry code is repeated three times with copy-paste. We need procedures (Chapter 7).
- There is no menu — the program runs once and exits. We need
casestatements and a main loop (Chapters 5 and 6). - Data is lost when the program exits. We need file I/O (Chapter 13).
Each of these limitations will be addressed in future chapters. For now, we have accomplished something significant: a program that talks to a human being, reads what they say, validates it, and presents the results clearly. That is real programming.
📊 Theme: Living Language (T3). The
Valprocedure we used for input validation is not part of the original 1970 Pascal standard — it was introduced by Borland in Turbo Pascal and has been supported by Free Pascal ever since. This is how a living language works: practical features that real programmers need become part of the standard toolkit. Free Pascal does not abandon useful extensions just because they were not in Wirth's original spec.
4.9 Chapter Summary
This chapter transformed our programs from monologues into dialogues. Here is what we covered:
Output:
- WriteLn writes text and moves to the next line. Write writes text and stays on the same line.
- Both accept multiple arguments of any type, separated by commas.
- Real numbers display in scientific notation by default — always format them explicitly.
Formatting:
- The :width specifier right-justifies a value in a field of at least width characters.
- The :width:decimals specifier displays real numbers in fixed-point notation with the specified number of decimal places.
- Use :0:2 for compact display, :10:2 (or similar) for tabular display.
- Field widths are minimums, never maximums — values wider than the field are never truncated.
Input:
- ReadLn reads input and discards the rest of the line (including the newline). Always prefer this for interactive programs.
- Read reads input but leaves the rest of the line in the buffer. Use only for file I/O or multi-value reads, and follow with a bare ReadLn.
- The prompt pattern is Write('prompt: '); ReadLn(variable);.
The Input Buffer:
- Characters typed by the user wait in a buffer until read.
- Read leaves the newline in the buffer, causing the next Read to behave unexpectedly.
- ReadLn clears the buffer. Use it.
Input Validation:
- Read user input as a string, then convert with Val(str, num, code).
- If code = 0, conversion succeeded. Otherwise, the input was invalid.
- Always validate before processing.
Text-Based UI: - Use headers, separators, and blank lines for visual structure. - Use field-width formatting for aligned columns. - The CRT unit provides colors and screen control (Free Pascal extension).
Spaced Review: Concepts from Previous Chapters
Before you move on, test your recall:
From Chapter 2: What is the difference between a compiled and an interpreted language? Why does Pascal's compiled nature matter for the programs we wrote in this chapter?
The distinction is direct: a compiled language (like Pascal) is translated entirely into machine code before execution, while an interpreted language (like Python) is translated line-by-line during execution. For I/O specifically, this means our Write and WriteLn calls compile directly to system calls — there is no interpreter overhead between your program and the operating system. Pascal programs print to the screen as fast as the terminal can accept the characters.
From Chapter 3: Why does Pascal require you to declare variable types before using them? How did this matter when we read user input in this chapter?
Type declarations serve as a contract between you and the compiler. When you declare amount: Real, you are saying "this variable will hold a floating-point number." When ReadLn(amount) encounters non-numeric input, Pascal knows something is wrong because the input violates the declared type. Without declared types, the program could not know what constitutes valid input — it would have to guess, and guessing is how bugs are born.
What Comes Next
In Chapter 5, we will learn to make decisions. Our programs will branch based on conditions — is the expense over budget? Which menu option did the user choose? Is the input valid? We will meet if-then-else and case statements, and PennyWise will gain a working menu system.
The combination of I/O (this chapter) and decisions (next chapter) is what transforms a calculator into a program. You are almost there.