ZetCode

C# String Format

last modified October 18, 2026

The C# String Format tutorial shows how to format strings in C#. We use string.Format, Console.WriteLine, and StringBuilder.AppendFormat to format strings.

Another way to create strings is to use string interpolation, which is covered at the end of this tutorial as well.

Composite formatting allows us to format strings in C#. It is supported by methods such as string.Format, Console.WriteLine, and StringBuilder.AppendFormat. A method takes a composite format string and a list of objects as input. The format string consists of fixed text and some format items. The format items are indexed placeholders which correspond to the objects in the list.

The format item has the following syntax:

{index[,alignment][:formatString]}

The index component is mandatory. It is a number starting from 0 that refers to an item from the list of objects. Multiple items can refer to the same element of the list. An object is ignored if it is not referenced by a format item. If we refer outside the bounds of the list of objects, a FormatException is thrown at run time.

The alignment component is optional. It is the minimum number of characters in the string representation of the argument. If positive, the argument is right-aligned; if negative, it is left-aligned. It is separated from the index with a comma.

The formatString is optional and follows a colon. It is a string that formats the value in a specific way. It can format numbers, dates and times, time spans, enumerations, GUIDs, and any type that implements IFormattable.

Note: the output of the examples depends on the current culture, which decides the group and decimal separators, the date patterns and the currency symbol. The examples set the culture to en-US explicitly or pass a culture to the method. Also be aware that the default number of decimal places of the N and P specifiers differs between platforms. On Windows, they are two; on Linux and macOS, .NET uses the data from ICU, which gives three for en-US. Always state the number of decimals, as in N2 and P2, if the output must be the same everywhere.

C# string format methods

In the following example, we use string.Format, Console.WriteLine, and StringBuilder.AppendFormat to format strings.

Program.cs
using System.Text;

var msg = string.Format("There are {0} owls", 5);
Console.WriteLine(msg);

Console.WriteLine("There are {0} eagles", 3);

var builder = new StringBuilder();
builder.AppendFormat("There are {0} hawks", 4);
Console.WriteLine(builder);

// the other writers have the same overloads
var sw = new StringWriter();
sw.WriteLine("There are {0} falcons", 6);
Console.Write(sw);

// more than three arguments, and an array
Console.WriteLine(string.Format("{0}, {1}, {2}, {3}, {4}", 1, 2, 3, 4, 5));
object[] items = ["a", "b"];
Console.WriteLine(string.Format("{0} and {1}", items));

The example formats strings using several methods.

var sw = new StringWriter();
sw.WriteLine("There are {0} falcons", 6);

Any TextWriter has the composite formatting overloads of Write and WriteLine, such as StreamWriter.

object[] items = ["a", "b"];
Console.WriteLine(string.Format("{0} and {1}", items));

The arguments can be passed as an array of objects. This is how the methods with more than three arguments work.

$ dotnet run
There are 5 owls
There are 3 eagles
There are 4 hawks
There are 6 falcons
1, 2, 3, 4, 5
a and b

C# string format index

In the following example, we format several strings using only the index component of the format item.

Program.cs
int oranges = 2;
int apples = 4;
int bananas = 3;

string str1 = "There are {0} oranges, {1} apples and {2} bananas";
string str2 = "There are {1} oranges, {2} bananas and {0} apples";

Console.WriteLine(str1, oranges, apples, bananas);
Console.WriteLine(str2, apples, oranges, bananas);

// an item can be used more than once, and an argument may be left out
Console.WriteLine("{0} + {0} = {1}", 2, 4);
Console.WriteLine("only {1}", "unused", "used");

// an index outside of the list throws
try
{
    Console.WriteLine("{0} {1}", "one");
}
catch (FormatException e)
{
    Console.WriteLine(e.Message);
}

// braces are escaped by doubling them
Console.WriteLine("{{0}} is a placeholder, {0} is a value", 42);

We print simple messages to the console.

string str1 = "There are {0} oranges, {1} apples and {2} bananas";

The {0}, {1}, and {2} are format items. We specify the index component. The other components are optional.

Console.WriteLine(str1, oranges, apples, bananas);

Now we put together the composite formatting. We have the string and the list of objects (oranges, apples, bananas). The {0} format item refers to the oranges. The WriteLine method replaces the {0} format item with the contents of the oranges variable.

string str2 = "There are {1} oranges, {2} bananas and {0} apples";

The order of the format items referring to the objects is important. The indexes let us put the text in a different order from the one of the arguments, which is useful for translations.

Console.WriteLine("{{0}} is a placeholder, {0} is a value", 42);

The curly brackets have a special meaning; to print them literally, we double them.

$ dotnet run
There are 2 oranges, 4 apples and 3 bananas
There are 2 oranges, 3 bananas and 4 apples
2 + 2 = 4
only used
Index (zero based) must be greater than or equal to zero and less than the size of the argument list.
{0} is a placeholder, 42 is a value

C# string format integers

The next example formats integers in the decimal and the hexadecimal notation.

Program.cs
Console.WriteLine("{0}  {1, 12}", "Decimal", "Hexadecimal");

Console.WriteLine("{0:D}  {1,8:X}", 502, 546);
Console.WriteLine("{0:D}  {1,8:X}", 345, 765);
Console.WriteLine("{0:D}  {1,8:X}", 320, 654);
Console.WriteLine("{0:D}  {1,8:X}", 120, 834);
Console.WriteLine("{0:D}  {1,8:X}", 620, 454);

Console.WriteLine("{0:D6} {1:X4} {2:x} {3:B8}", 42, 255, 255, 5);

We print numbers in a decimal and a hexadecimal format. We also align the numbers using the alignment component.

Console.WriteLine("{0:D}  {1,8:X}", 502, 546);

The {0:D} format item specifies that the first item from the list of supplied objects will be formatted in the decimal format. The {1,8:X} format item takes the second item and formats it in the hexadecimal format :X. The alignment of 8 means that the field is eight characters wide. Because the number has only three characters, it is right-aligned and padded with spaces.

Console.WriteLine("{0:D6} {1:X4} {2:x} {3:B8}", 42, 255, 255, 5);

A number after the specifier is the minimum number of digits; the result is padded with zeros. The lowercase x gives lowercase hexadecimal digits, and B (.NET 8) formats a binary number.

$ dotnet run
Decimal   Hexadecimal
502       222
345       2FD
320       28E
120       342
620       1C6
000042 00FF ff 00000101

C# string format standard numeric formats

The following example shows the standard numeric format specifiers.

Program.cs
using System.Globalization;

CultureInfo.CurrentCulture = new CultureInfo("en-US");

Console.WriteLine("Number: {0:N2}", 1270.5);
Console.WriteLine("Fixed: {0:F3}", 1270.5);
Console.WriteLine("Scientific: {0:E}", 127);
Console.WriteLine("Currency: {0:C}", 127);
Console.WriteLine("Percent: {0:P2}", 0.127);
Console.WriteLine("Percent, no decimals: {0:P0}", 1.27);
Console.WriteLine("Hexadecimal: {0:X}", 127);
Console.WriteLine("General: {0:G}", 0.000012345);
Console.WriteLine("Round-trip: {0:R}", 0.1 + 0.2);
Console.WriteLine("Precision: {0:N0} {0:N1} {0:N4}", 2.5);
Console.WriteLine("Negative: {0:C} {0:N2}", -1234.5);
Console.WriteLine("Big: {0:N0}", 1_234_567_890L);

The example demonstrates the standard format specifiers for numbers: number, fixed, scientific, currency, percent, hexadecimal, general, and round-trip.

Console.WriteLine("Percent: {0:P2}", 0.127);
Console.WriteLine("Percent, no decimals: {0:P0}", 1.27);

The percent specifier multiplies the number by 100. Printing 127 with P gives 12,700%, a common mistake.

Console.WriteLine("Round-trip: {0:R}", 0.1 + 0.2);

The sum of 0.1 and 0.2 is not exactly 0.3 in binary floating-point arithmetic. R prints enough digits that the number can be read back to the same value, which exposes the difference. The default ToString of a double in .NET Core 3.0 and later also prints the shortest round-trippable string.

$ dotnet run
Number: 1,270.50
Fixed: 1270.500
Scientific: 1.270000E+002
Currency: $127.00
Percent: 12.70%
Percent, no decimals: 127%
Hexadecimal: 7F
General: 1.2345E-05
Round-trip: 0.30000000000000004
Precision: 2 2.5 2.5000
Negative: -$1,234.50 -1,234.50
Big: 1,234,567,890

The comma and dot specifiers

The custom numeric format strings describe the output with placeholders. The dot (.) specifier inserts a localized decimal separator into the result string. The comma (,) specifier inserts a group separator; at the end of the number, it scales the number down by 1000. The 0 placeholder shows a digit or zero, the # placeholder shows a digit or nothing.

Program.cs
using System.Globalization;

double val = 127723134.212578;
var inv = CultureInfo.InvariantCulture;

Console.WriteLine(string.Format(inv, "{0:#,#.##}", val));
Console.WriteLine(string.Format(inv, "{0:0.0000}", val));
Console.WriteLine(string.Format(inv, "{0:0,0.0}", val));
Console.WriteLine(string.Format(inv, "{0:000000.00}", 42.5));
Console.WriteLine(string.Format(inv, "{0:#.##}", 0.5));
Console.WriteLine(string.Format(inv, "{0:0.##}", 0.5));
Console.WriteLine(string.Format(inv, "{0:0.##}", 5));
Console.WriteLine(string.Format(inv, "{0:0.00%}", 0.4567));
Console.WriteLine(string.Format(inv, "{0:#,##0,}K", 1234567));          // a trailing comma scales by 1000
Console.WriteLine(string.Format(inv, "{0:+0;-0;zero}", 5));
Console.WriteLine(string.Format(inv, "{0:+0;-0;zero}", -5));
Console.WriteLine(string.Format(inv, "{0:+0;-0;zero}", 0));
Console.WriteLine(string.Format(inv, "{0:(###) ###-####}", 5551234567));
Console.WriteLine(string.Format(inv, "{0:0.0e+0}", 12345.678));

// the same number in other cultures
foreach (var name in new[] { "en-US", "de-DE", "fr-FR", "sk-SK" })
{
    Console.WriteLine($"{name}: {string.Format(new CultureInfo(name), "{0:N2}", val)}");
}

We format a double value with several custom formats, and then with different cultures.

Console.WriteLine(string.Format(inv, "{0:#,#.##}", val));

This format uses a group separator and displays at most two digits after the decimal point.

Console.WriteLine(string.Format(inv, "{0:0.0000}", val));

In this format, we display four decimal places and no group separator.

Console.WriteLine(string.Format(inv, "{0:#.##}", 0.5));
Console.WriteLine(string.Format(inv, "{0:0.##}", 0.5));

The difference between # and 0: the first prints ".5", the second "0.5". With 0.## the whole number 5 prints as "5", without a trailing point.

Console.WriteLine(string.Format(inv, "{0:+0;-0;zero}", 5));

A custom format can have up to three sections separated by semicolons: for positive numbers, negative numbers, and zero. This one prints a sign for positive numbers and the word "zero" for zero.

Console.WriteLine(string.Format(inv, "{0:(###) ###-####}", 5551234567));

Other characters in the format string are copied to the output, so we can format a phone number.

$ dotnet run
127,723,134.21
127723134.2126
127,723,134.2
000042.50
.5
0.5
5
45.67%
1,235K
+5
-5
zero
(555) 123-4567
1.2e+4
en-US: 127,723,134.21
de-DE: 127.723.134,21
fr-FR: 127 723 134,21
sk-SK: 127 723 134,21

The last four lines show the same number in four cultures: the group and decimal separators differ (the French and Slovak group separator is a no-break space).

C# string format alignment

The alignment (or width) component is the minimum number of characters to be written to the output. If we use {0,10}, the output is right-aligned. For left alignment, we specify a negative width: {0,-10}.

Program.cs
Console.WriteLine(1);
Console.WriteLine(16);
Console.WriteLine(1655);
Console.WriteLine(16567);
Console.WriteLine(166701);

Console.WriteLine("---");

Console.WriteLine("{0,10}", 1);
Console.WriteLine("{0,10}", 16);
Console.WriteLine("{0,10}", 1655);
Console.WriteLine("{0,10}", 16567);
Console.WriteLine("{0,10}", 166701);

Console.WriteLine("---");

Console.WriteLine("|{0,-10}|{1,10}|{2,-6:N1}|", "left", "right", 3.14159);

// a table
(string Name, int Qty, decimal Price)[] items = [("Apple", 3, 0.5m), ("Watermelon", 1, 3.25m), ("Cherry", 120, 0.02m)];

Console.WriteLine("{0,-12}{1,5}{2,10}", "Item", "Qty", "Total");
foreach (var (name, qty, price) in items)
{
    Console.WriteLine("{0,-12}{1,5}{2,10:C}", name, qty, qty * price);
}

// text longer than the width is not truncated
Console.WriteLine("|{0,4}|", "too long text");

First, we print five numbers without specifying the width. The length of the output is equal to the number of the characters being displayed. In the second case, we have a width of 10. Each of the five outputs has a minimum length of ten characters. The numbers are right-aligned. Then we mix alignments and formats in one line and print a small table.

Console.WriteLine("{0,-12}{1,5}{2,10:C}", name, qty, qty * price);

The name is left-aligned in twelve characters, the quantity is right-aligned in five, and the total is a currency value right-aligned in ten. The columns of the table line up with a monospaced font.

Console.WriteLine("|{0,4}|", "too long text");

The width is a minimum. Longer text is never truncated.

$ dotnet run
1
16
1655
16567
166701
---
         1
        16
      1655
     16567
    166701
---
|left      |     right|3.1   |
Item          Qty     Total
Apple           3     $1.50
Watermelon      1     $3.25
Cherry        120     $2.40
|too long text|

C# string format date and time

The next example formats date and time data. The dates and times can be formatted with the standard format specifiers (d, D, t, T, and so on) or custom patterns made of yyyy, MM, dd, HH, mm, and ss. The date and time tutorial has the full list.

Program.cs
using System.Globalization;

CultureInfo.CurrentCulture = new CultureInfo("en-US");

var dt = new DateTime(2024, 1, 17, 9, 55, 43);

Console.WriteLine("Short date: {0:d}", dt);
Console.WriteLine("Long date: {0:D}", dt);
Console.WriteLine("Short time: {0:t}", dt);
Console.WriteLine("Long time: {0:T}", dt);
Console.WriteLine("Month: {0:M}", dt);
Console.WriteLine("Year: {0:Y}", dt);
Console.WriteLine("ISO 8601: {0:s}", dt);
Console.WriteLine("Custom: {0:yyyy-MM-dd HH:mm}", dt);
Console.WriteLine("Custom 2: {0:dddd, d MMMM yyyy}", dt);
Console.WriteLine("Slovak: {0}", string.Format(new CultureInfo("sk-SK"), "{0:D}", dt));
Console.WriteLine("TimeSpan: {0:hh\\:mm\\:ss} {1:c}", new TimeSpan(1, 2, 3), new TimeSpan(1, 2, 3, 4));
Console.WriteLine("DateOnly: {0:yyyy/MM/dd}", new DateOnly(2024, 1, 17));

The code example shows several formats of a fixed date, and also formats a TimeSpan and a DateOnly.

Console.WriteLine("TimeSpan: {0:hh\\:mm\\:ss} {1:c}", new TimeSpan(1, 2, 3), new TimeSpan(1, 2, 3, 4));

In the custom format of a TimeSpan, the colon must be escaped with a backslash (doubled in a normal string literal). The c specifier is the constant, culture-independent format.

$ dotnet run
Short date: 1/17/2024
Long date: Wednesday, January 17, 2024
Short time: 9:55 AM
Long time: 9:55:43 AM
Month: January 17
Year: January 2024
ISO 8601: 2024-01-17T09:55:43
Custom: 2024-01-17 09:55
Custom 2: Wednesday, 17 January 2024
Slovak: streda 17. januára 2024
TimeSpan: 01:02:03 1.02:03:04
DateOnly: 2024/01/17

C# string format enums, booleans and GUIDs

Enumerations are formatted by name by default. The D specifier gives the number and X the hexadecimal value. Flag enumerations print the names of all set flags. A GUID has several formats.

Program.cs
Console.WriteLine("{0}", Color.Red);
Console.WriteLine("{0:D}", Color.Red);
Console.WriteLine("{0:X}", Color.Blue);
Console.WriteLine("{0:G}", Color.Green);
Console.WriteLine("{0}", Perms.Read | Perms.Write);
Console.WriteLine("{0:D}", Perms.Read | Perms.Write);

Console.WriteLine("{0} {1} {2}", true, 'c', null);
Console.WriteLine("{0:N2} {1}", 1234.5m, Guid.Empty);
Console.WriteLine("{0:D} {0:N} {0:B}", Guid.Empty);

enum Color { Red = 1, Green = 2, Blue = 11 }

[Flags]
enum Perms { Read = 1, Write = 2, Exec = 4 }

The program formats enums, flags, and other values.

$ dotnet run
Red
1
0000000B
Green
Read, Write
3
True c
1,234.50 00000000-0000-0000-0000-000000000000
00000000-0000-0000-0000-000000000000 00000000000000000000000000000000 {00000000-0000-0000-0000-000000000000}

A null argument is formatted as an empty string. For a GUID, D is the default hyphenated form, N has no hyphens, and B adds braces.

C# string format and cultures

Without a format provider, the current culture of the thread decides how numbers and dates are written. This is what we want for output for people, and exactly what we do not want for files and network protocols.

Program.cs
using System.Globalization;

var inv = CultureInfo.InvariantCulture;
var de = new CultureInfo("de-DE");
var us = new CultureInfo("en-US");

double price = 1234.56;

Console.WriteLine(string.Format(us, "{0:C}", price));
Console.WriteLine(string.Format(de, "{0:C}", price));
Console.WriteLine(string.Format(new CultureInfo("ja-JP"), "{0:C0}", price));
Console.WriteLine(string.Format(inv, "{0:C}", price));
Console.WriteLine(string.Format(new CultureInfo("en-GB"), "{0:C}", price));

// the current culture decides when no provider is given
CultureInfo.CurrentCulture = de;
Console.WriteLine("{0:N2} / {0}", price);
CultureInfo.CurrentCulture = us;
Console.WriteLine("{0:N2} / {0}", price);

// for files and protocols always use the invariant culture
Console.WriteLine(string.Format(inv, "{0}", price));

The program formats a price in several cultures and shows the effect of the current culture.

Console.WriteLine(string.Format(inv, "{0:C}", price));

The invariant culture uses the generic currency sign (¤), which is not a real currency; it is meant for data, not for display.

$ dotnet run
$1,234.56
1.234,56 €
¥1,235
¤1,234.56
£1,234.56
1.234,56 / 1234,56
1,234.56 / 1234.56
1234.56

Note the symbol after the number and the decimal comma in the German format. The yen has no minor unit, so we asked for no decimals with C0. To produce the same text on every machine, pass CultureInfo.InvariantCulture.

C# string interpolation and CompositeFormat

String interpolation (C# 6) puts the expressions directly into the string, with the same alignment and format components: {expression,alignment:format}. The compiler turns an interpolated string into a call of string.Format or, since C# 10, into a more efficient interpolated string handler. For a format string that is used many times, CompositeFormat (.NET 8) parses it just once.

Program.cs
using System.Text;

// CompositeFormat parses the format string once; it is faster when the same format is used many times
var format = CompositeFormat.Parse("{0,-8}|{1,6:N2}|");

for (int i = 1; i <= 3; i++)
{
    Console.WriteLine(string.Format(null, format, $"row{i}", i * 1.5));
}

// interpolated strings are compiled to the efficient handler, with alignment and formats
string name = "falcon";
double speed = 389.5;
Console.WriteLine($"{name,-10}|{speed,8:F1}|");

// a FormattableString keeps the format and the arguments
FormattableString fs = $"{name} flies at {speed:F0} km/h";
Console.WriteLine(fs.Format);
Console.WriteLine(fs.ArgumentCount);
Console.WriteLine(fs.ToString(System.Globalization.CultureInfo.InvariantCulture));

// string.Create with an interpolation handler and a provider
var s = string.Create(System.Globalization.CultureInfo.InvariantCulture, $"{speed:N1}");
Console.WriteLine(s);

// the format string cannot be built from user input without care: a bad format throws
string userFormat = "{0:Q}";
try
{
    Console.WriteLine(string.Format(userFormat, 5));
}
catch (FormatException e)
{
    Console.WriteLine(e.Message);
}

The program shows CompositeFormat, interpolated strings, FormattableString, and an invalid format.

var format = CompositeFormat.Parse("{0,-8}|{1,6:N2}|");
Console.WriteLine(string.Format(null, format, $"row{i}", i * 1.5));

The format is parsed once into a CompositeFormat object, and the string.Format overload that takes it skips the parsing in every call. The first argument is the format provider; null means the current culture.

FormattableString fs = $"{name} flies at {speed:F0} km/h";
Console.WriteLine(fs.Format);

Assigned to a FormattableString, the interpolated string is not formatted yet. It keeps the composite format and the arguments, which we can format with any culture. This is the basis of the safe query libraries that turn interpolated strings into SQL parameters.

string userFormat = "{0:Q}";

Format strings from users or files can be invalid, and then a FormatException is thrown. When the format comes from outside, catch the exception.

$ dotnet run
row1    |  1.50|
row2    |  3.00|
row3    |  4.50|
falcon    |   389.5|
{0} flies at {1:F0} km/h
2
falcon flies at 390 km/h
389.5
Format specifier was invalid.

C# custom formatting with IFormattable

A type controls its own formatting by implementing IFormattable. Its ToString(format, provider) method receives the format string of the format item. To change the formatting of types we do not control, we write an ICustomFormatter and pass it as the provider.

Program.cs
using System.Globalization;

// IFormattable: a type decides how it formats itself
var p = new Point(3, 4);
Console.WriteLine("{0}", p);
Console.WriteLine("{0:F}", p);
Console.WriteLine("{0:P}", p);
Console.WriteLine("{0:F}", new Point(1.5, 2.25));
Console.WriteLine($"{p:P}");

// ICustomFormatter: a provider changes the formatting of other types
Console.WriteLine(string.Format(new YesNoFormatter(), "Active: {0}, Admin: {1}, Count: {2}", true, false, 3));

record Point(double X, double Y) : IFormattable
{
    public string ToString(string? format, IFormatProvider? provider)
    {
        provider ??= CultureInfo.CurrentCulture;
        return format switch
        {
            "F" => $"({X.ToString("F2", provider)}, {Y.ToString("F2", provider)})",
            "P" => $"r={Math.Sqrt(X * X + Y * Y).ToString("F1", provider)}",
            null or "" or "G" => $"({X.ToString(provider)}, {Y.ToString(provider)})",
            _ => throw new FormatException($"Unknown format '{format}'.")
        };
    }

    public override string ToString() => ToString(null, null);
}

class YesNoFormatter : IFormatProvider, ICustomFormatter
{
    public object? GetFormat(Type? formatType) => formatType == typeof(ICustomFormatter) ? this : null;

    public string Format(string? format, object? arg, IFormatProvider? provider) =>
        arg is bool b ? (b ? "yes" : "no") : arg is IFormattable f ? f.ToString(format, CultureInfo.InvariantCulture) : arg?.ToString() ?? "";
}

The Point record supports the formats "F" and "P", and a custom formatter prints Booleans as yes and no.

Console.WriteLine(string.Format(new YesNoFormatter(), "Active: {0}, Admin: {1}, Count: {2}", true, false, 3));

The provider's GetFormat method returns the formatter when string.Format asks for an ICustomFormatter. The formatter is then called for every argument, and it passes the other types on to their own formatting.

$ dotnet run
(3, 4)
(3.00, 4.00)
r=5.0
(1.50, 2.25)
r=5.0
Active: yes, Admin: no, Count: 3

Source

Composite formatting

In this article we have formatted strings in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.