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.
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.
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.
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.
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.
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.
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}.
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.
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.
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.
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.
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.
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
In this article we have formatted strings in C#.
Author
List all C# tutorials.