Last Updated: June 6, 2026
Every program you write in this course will, sooner or later, need to talk to the user. The simplest way to do that in C# is through the console: print messages, read what the user types, format numbers nicely, and react to bad input without crashing. This chapter covers the input and output features of Console that you'll use in almost every small program you write, with formatting and parsing patterns that scale up cleanly later.
The Console class lives in the System namespace and represents the standard input/output streams of the terminal. The two most-used methods are Console.WriteLine (writes a value and adds a newline) and Console.Write (writes with no newline). The difference matters more than it looks like it should.
WriteLine finishes each line, so the second message lands on its own line. Use Write when you want to build a single line piece by piece, or when prompting the user so the cursor stays on the same line as the question.
There's also Console.Error.WriteLine, which writes to standard error (stderr) instead of standard output (stdout). The visible result in the terminal looks the same, but it lets shell scripts, CI pipelines, and log aggregators separate diagnostic messages from regular output.
A user redirecting the program output with dotnet run > orders.txt would see only Order placed successfully. in the file. The warning still appears in the terminal because it went to stderr, which isn't being redirected.
C# has had a placeholder-style formatting syntax since version 1. You pass a format string with numbered placeholders like {0} and {1}, followed by the values that fill them in.
The numbers in the braces are positional indexes into the argument list. {0} picks the first argument after the format string, {1} the second, and so on. The same index can appear more than once: "{0} and {0}" repeats the same value twice.
After the index, a colon introduces a format specifier that controls how the value is rendered. {2:C} means "format argument 2 as currency." The format specifier is what turns 49.99 into $49.99.
The standard numeric format specifiers cover most everyday cases:
| Specifier | Meaning | Input 1234.5678 | Notes |
|---|---|---|---|
C | Currency | $1,234.57 | Uses the current culture's symbol and rounding |
N | Number with thousands separators | 1,234.57 | Two decimal places by default |
N2 | Number, two decimals | 1,234.57 | Digit after N sets decimal places |
F2 | Fixed-point, two decimals | 1234.57 | No thousands separator |
P | Percent | 123,456.78 % | Multiplies by 100 first |
D5 | Decimal padding (integers only) | 01234 | D only works on integer types |
E | Exponential (scientific) | 1.234568E+003 | Capital E in output |
X | Hexadecimal (integers only) | 4D2 | X for uppercase, x for lowercase |
A couple of these have specific rules. D and X only accept integer types, not decimal or double. P multiplies the value by 100 before rendering, so 0.25 formats as 25.00 %, not 0.25 %. For a value already as a percentage like 25.0, divide by 100 before formatting.
You can also add an alignment specifier before the colon. Positive numbers right-align, negatives left-align, and the absolute value is the minimum field width:
{0,-10} reserves 10 characters and left-pads the product name with spaces on the right. {1,10:C} reserves 10 characters and right-aligns the currency value. That's enough to produce a clean little invoice table without any extra string-building work.
Composite formatting works, but counting placeholders gets old fast. Since C# 6, you can prefix a string with $ and embed expressions directly inside braces. This is called string interpolation, and it's the modern preference for almost all formatted output.
The format specifiers and alignment work exactly the same way. The expression inside the braces can be any valid C# expression, not just a variable:
Composite formatting still has its place. The static string.Format method, logging frameworks, and resource files all use the same {0} placeholder syntax, so understanding both forms pays off. For inline Console.WriteLine calls in new code, interpolation is cleaner and easier to read.
Each $"..." builds a new string by concatenating its pieces. Inside a tight loop that runs millions of times, prefer a StringBuilder or a logging API that handles formatting lazily.
The mirror image of WriteLine is Console.ReadLine. It reads one line of text from standard input (everything the user types, up to and including the Enter key) and returns the line as a string. The return type is string?, not string, because input streams can end. If the user closes the input (Ctrl+D on Unix, Ctrl+Z on Windows) or the program is reading from a file that runs out, ReadLine returns null.
Sample run:
The ? in string? is the nullable reference type annotation introduced in C# 8. The compiler warns you if you call members on name without first checking for null. That's a feature, not a nuisance, because forgetting that input can be missing is one of the easier ways to crash a program in production.
For single keystrokes (think "Press any key to continue..."), Console.ReadKey reads one key press and returns a ConsoleKeyInfo describing which key was pressed.
Sample run:
Console.ReadKey() echoes the key by default and returns immediately, no Enter needed. Pass true (Console.ReadKey(intercept: true)) to suppress the echo, which is useful for hidden prompts like passwords or menu choices where you don't want the character showing.
ReadLine always gives you a string. Most of the time, you want a number, a date, or some other typed value. That means parsing the string, and parsing means anticipating bad input. There are two parsing patterns you'll use constantly.
The first is int.Parse, which throws a FormatException if the input isn't a valid integer:
Sample run with valid input:
Sample run with bad input:
A crash on bad input is rarely what you want. The second pattern, int.TryParse, handles invalid input gracefully:
Sample run with bad input:
TryParse returns a bool and writes the result to an out parameter. If parsing succeeds, the return value is true and qty holds the number. If it fails, the return value is false and qty is 0. There's no exception to catch, no crash to handle. Prefer TryParse for user input, and reserve Parse for cases where the value comes from a trusted source you've already validated.
The same pattern exists for every numeric type: decimal.TryParse, double.TryParse, DateTime.TryParse, bool.TryParse, and so on. The shape is identical: Type.TryParse(input, out var value).
A common follow-up is looping until the user types something valid:
Sample run:
The loop keeps prompting until both conditions pass: the input parses cleanly and the value is positive. That two-step validation (parse, then check business rules) is the standard shape for any "read until valid" pattern.
A single ReadLine reads one line. If you want several values on the same line (say, a width and a height separated by a space), you can split the string and parse each piece.
Sample run:
Split(' ') returns an array of substrings divided by the space character. The ?. and ?? combination handles the case where ReadLine returned null. After that, every condition has to pass before we trust the data: two parts, a valid integer, and a valid decimal. If anything fails, the user sees a friendly error instead of a crash.
For a culture-sensitive parse (for example, parsing 19,99 as 19.99 in countries that use commas as decimal separators), pass a CultureInfo:
The same applies to formatting: price.ToString("C", CultureInfo.GetCultureInfo("de-DE")) renders as 19,99 €. When you don't pass a culture, both parsing and formatting use the current thread's culture, which depends on the operating system's regional settings. The Strings section covers strings and formatting in more depth.
Putting the pieces together, here's a small program that prompts for a quantity and a unit price, validates the input, and prints a formatted invoice. This is the kind of program you'll write a hundred variants of during the course.
Sample run:
Everything we've covered shows up here. Console.Write for prompts, ReadLine for input, TryParse for safe conversion, a while (true) loop with a break for "read until valid," string interpolation with alignment and currency format for the invoice. The flow from user input to formatted output is the basic shape of any console program.
Here's how those pieces fit together as a flow:
The diagram traces a single round trip. Input flows up through stdin and gets converted from a string into a typed value before the program touches it. Output flows back down through stdout after being formatted into something the user can read. Every console program you write follows some version of this loop.
A handful of other Console members come up often enough to know about, even if you won't use them every day:
| Member | What it does |
|---|---|
Console.Clear() | Clears the terminal screen and moves the cursor to the top-left corner. |
Console.ForegroundColor | Gets or sets the text color. Assign a ConsoleColor value (Red, Green, etc.). |
Console.BackgroundColor | Gets or sets the background color for new text. |
Console.ResetColor() | Restores foreground and background to the terminal defaults. |
Console.Title | Gets or sets the terminal window title. Windows only, no-op on macOS and Linux. |
Console.WindowWidth | Gets the width of the console window in characters. Useful for centering output. |
Console.Beep() | Plays the system beep sound. |
A quick demo of color output:
Colors only affect interactive terminals. If output is piped or redirected to a file, the color codes are stripped (or, in some environments, written as raw escape sequences). Don't rely on color to convey meaning, treat it as a nice-to-have on top of plain text.
10 quizzes