diff --git a/mido/frozen.py b/mido/frozen.py
index 2c04f1f..20d629d 100644
--- a/mido/frozen.py
+++ b/mido/frozen.py
@@ -3,10 +3,15 @@ from .midifiles import MetaMessage, UnknownMetaMessage
 
 
 class Frozen(object):
-    def __repr__(self):
-        text = super(Frozen, self).__repr__()
+    def __str__(self):
+        text = super(Frozen, self).__str__()
         return '<frozen {}'.format(text[1:])
 
+    def __repr__(self):
+        # canonicalize to mutable objects
+        r = super(Frozen, self).__repr__()
+        return r[len('Frozen'):]
+
     def __setattr__(self, *_):
         raise ValueError('frozen message is immutable')
 
diff --git a/mido/messages/messages.py b/mido/messages/messages.py
index f8f7e36..f1387d4 100644
--- a/mido/messages/messages.py
+++ b/mido/messages/messages.py
@@ -54,6 +54,15 @@ class BaseMessage(object):
         """
         return cl(**data)
 
+    def __repr__(self):
+        d = self.dict()
+        msg_type = d.pop('type')
+        items = getattr(d, 'iteritems', d.items)
+        return "%s('%s', %s)" % (
+            type(self).__name__,
+            msg_type,
+            ', '.join('%s=%s' % item for item in items()))
+
     @property
     def is_realtime(self):
         """True if the message is a system realtime message."""
@@ -162,9 +171,6 @@ class Message(BaseMessage):
     def __str__(self):
         return msg2str(vars(self))
 
-    def __repr__(self):
-        return '<message {}>'.format(str(self))
-
     def _setattr(self, name, value):
         if name == 'type':
             raise AttributeError('type attribute is read only')
diff --git a/mido/midifiles/meta.py b/mido/midifiles/meta.py
index be9552c..8b73da0 100644
--- a/mido/midifiles/meta.py
+++ b/mido/midifiles/meta.py
@@ -535,7 +535,7 @@ class MetaMessage(BaseMessage):
                 encode_variable_int(len(data)) +
                 data)
 
-    def __repr__(self):
+    def __str__(self):
         spec = _META_SPEC_BY_TYPE[self.type]
         attributes = []
         for name in spec.attributes:
@@ -561,7 +561,7 @@ class UnknownMetaMessage(MetaMessage):
             'data': data,
             'time': time})
 
-    def __repr__(self):
+    def __str__(self):
         return ('<unknown meta message'
                 ' type_byte=0x{:02x} '
                 'data={!r} time={}>').format(self.type_byte,
@@ -569,6 +569,11 @@ class UnknownMetaMessage(MetaMessage):
                                              self.time
                                              )
 
+    def __repr__(self):
+        # fix message type artifact
+        r = super(UnknownMetaMessage, self).__repr__()
+        return r.replace("'unknown_meta', ", '')
+
     def __setattr__(self, name, value):
         # This doesn't do any checking.
         # It probably should.
diff --git a/mido/midifiles/midifiles.py b/mido/midifiles/midifiles.py
index bb885a2..f3db7cb 100644
--- a/mido/midifiles/midifiles.py
+++ b/mido/midifiles/midifiles.py
@@ -293,7 +293,8 @@ class MidiFile(object):
                  type=1, ticks_per_beat=DEFAULT_TICKS_PER_BEAT,
                  charset='latin1',
                  debug=False,
-                 clip=False
+                 clip=False,
+                 tracks=None
                  ):
 
         self.filename = filename
@@ -309,7 +310,9 @@ class MidiFile(object):
             raise ValueError(
                 'invalid format {} (must be 0, 1 or 2)'.format(format))
 
-        if file is not None:
+        if tracks is not None:
+            self.tracks = tracks
+        elif file is not None:
             self._load(file)
         elif self.filename is not None:
             with io.open(filename, 'rb') as file:
@@ -461,11 +464,18 @@ class MidiFile(object):
                 else:
                     print('{!r}'.format(msg))
 
-    def __repr__(self):
+    def __str__(self):
         return '<midi file {!r} type {}, {} tracks, {} messages>'.format(
             self.filename, self.type, len(self.tracks),
             sum([len(track) for track in self.tracks]))
 
+    def __repr__(self):
+        tracks_str = ',\n'.join(repr(track) for track in self.tracks)
+        tracks_str = '\n'.join('  ' + line for line in tracks_str.splitlines())
+        tracks_str = (', tracks=[\n%s\n]' % tracks_str) if self.tracks else ''
+        return 'MidiFile(type=%s, ticks_per_beat=%s%s)' % (
+            self.type, self.ticks_per_beat, tracks_str)
+
     # The context manager has no purpose but is kept around since it was
     # used in examples in the past.
     def __enter__(self):
diff --git a/mido/midifiles/tracks.py b/mido/midifiles/tracks.py
index 59ad1df..6140688 100644
--- a/mido/midifiles/tracks.py
+++ b/mido/midifiles/tracks.py
@@ -50,9 +50,16 @@ class MidiTrack(list):
     def __mul__(self, other):
         return self.__class__(list.__mul__(self, other))
 
-    def __repr__(self):
+    def __str__(self):
         return '<midi track {!r} {} messages>'.format(self.name, len(self))
 
+    def __repr__(self):
+        messages = ''
+        if len(self) > 0:
+            template = '[\n  %s]' if len(self) > 1 else '[%s]'
+            messages = template % ',\n  '.join(repr(m) for m in self)
+        return 'MidiTrack(%s)' % messages
+
 
 def _to_abstime(messages):
     """Convert messages to absolute time."""

proper __repr__ for MidiFile, etc.
Per Python docs, `__repr__()` output "should normally look like a valid Python expression that can be used to recreate an object with the same value".

Currently for MidiFile, repr() gives the same result as str(), which is just a summary including number of tracks, etc.  Likewise MidiTrack gives a summary with number of messages.

One place a proper repr() would be immediately useful is for tests, e.g. to confirm that code changes don't alter generated MIDI messages, tracks, or files.  It would also facilitate viewing differences between MIDI data in text form.

With a proper repr(), it would also be trivial to implement MidiFile `__eq__` (see #150).
diff --git a/tests/messages/test_messages.py b/tests/messages/test_messages.py
index a834fbd..1b076c0 100644
--- a/tests/messages/test_messages.py
+++ b/tests/messages/test_messages.py
@@ -108,3 +108,9 @@ def test_dict_sysex_data():
 def test_from_hex_sysex_data_type():
     msg = Message.from_hex('F0 01 02 03 F7')
     assert isinstance(msg.data, SysexData)
+
+
+def test_repr():
+    msg = Message('note_on', channel=1, note=2, time=3)
+    msg_eval = eval(repr(msg))
+    assert msg == msg_eval
diff --git a/tests/midifiles/test_meta.py b/tests/midifiles/test_meta.py
index 3ea3c71..4ac6145 100644
--- a/tests/midifiles/test_meta.py
+++ b/tests/midifiles/test_meta.py
@@ -1,5 +1,5 @@
 import pytest
-from mido.midifiles.meta import MetaMessage, MetaSpec_key_signature, KeySignatureError
+from mido.midifiles.meta import MetaMessage, UnknownMetaMessage, MetaSpec_key_signature, KeySignatureError
 
 
 def test_copy_invalid_argument():
@@ -30,3 +30,13 @@ class TestKeySignature:
         msg = MetaMessage('key_signature')
         MetaSpec_key_signature().decode(msg, input_bytes)
         assert msg.key == expect_sig
+
+def test_meta_message_repr():
+    msg = MetaMessage('end_of_track', time=10)
+    msg_eval = eval(repr(msg))
+    assert msg == msg_eval
+
+def test_unknown_meta_message_repr():
+    msg = UnknownMetaMessage(type_byte=99, data=[1, 2], time=10)
+    msg_eval = eval(repr(msg))
+    assert msg == msg_eval
diff --git a/tests/midifiles/test_midifiles.py b/tests/midifiles/test_midifiles.py
index 8231e45..a579ed7 100644
--- a/tests/midifiles/test_midifiles.py
+++ b/tests/midifiles/test_midifiles.py
@@ -1,7 +1,7 @@
 import io
 from pytest import raises
 from mido.messages import Message
-from mido.midifiles.midifiles import MidiFile
+from mido.midifiles.midifiles import MidiFile, MidiTrack
 from mido.midifiles.meta import MetaMessage, KeySignatureError
 
 HEADER_ONE_TRACK = """
@@ -163,3 +163,19 @@ def test_meta_messages_with_length_0():
 
         MetaMessage('end_of_track'),
     ]
+
+
+def test_midifile_repr():
+    midifile = MidiFile(type=1, ticks_per_beat=123, tracks=[
+        MidiTrack([
+            Message('note_on', channel=1, note=2, time=3),
+            Message('note_off', channel=1, note=2, time=3)]),
+        MidiTrack([
+            MetaMessage('sequence_number', number=5),
+            Message('note_on', channel=2, note=6, time=9),
+            Message('note_off', channel=2, note=6, time=9)]),
+    ])
+    midifile_eval = eval(repr(midifile))
+    for track, track_eval in zip(midifile.tracks, midifile_eval.tracks):
+        for m1, m2 in zip(track, track_eval):
+            assert m1 == m2
diff --git a/tests/midifiles/test_tracks.py b/tests/midifiles/test_tracks.py
index 6dddcde..54467b8 100644
--- a/tests/midifiles/test_tracks.py
+++ b/tests/midifiles/test_tracks.py
@@ -1,6 +1,10 @@
+import itertools
+from mido.messages import Message
 from mido.midifiles.meta import MetaMessage
 from mido.midifiles.tracks import MidiTrack
 
+zip = getattr(itertools, 'izip', zip)
+
 
 def test_track_slice():
     track = MidiTrack()
@@ -16,3 +20,13 @@ def test_track_name():
     # The track should use the first name it finds.
     track = MidiTrack([name1, name2])
     assert track.name == name1.name
+
+
+def test_track_repr():
+    track = MidiTrack([
+        Message('note_on', channel=1, note=2, time=3),
+        Message('note_off', channel=1, note=2, time=3),
+    ])
+    track_eval = eval(repr(track))
+    for m1, m2 in zip(track, track_eval):
+        assert m1 == m2
diff --git a/tests/test_frozen.py b/tests/test_frozen.py
index b26f010..0551d2f 100644
--- a/tests/test_frozen.py
+++ b/tests/test_frozen.py
@@ -1,4 +1,5 @@
 from mido.messages import Message
+from mido.midifiles.meta import MetaMessage, UnknownMetaMessage
 from mido.frozen import (is_frozen, freeze_message, thaw_message,
                          FrozenMessage, FrozenMetaMessage,
                          FrozenUnknownMetaMessage)
@@ -27,3 +28,24 @@ def test_thawed_message_is_copy():
 def test_is_frozen():
     assert is_frozen(FrozenMessage('note_on'))
     assert not is_frozen(Message('note_on'))
+
+
+def test_frozen_repr():
+    msg = FrozenMessage('note_on', channel=1, note=2, time=3)
+    msg_eval = eval(repr(msg))
+    assert type(msg_eval) == Message
+    assert msg == msg_eval
+
+
+def test_frozen_meta_repr():
+    msg = FrozenMetaMessage('end_of_track', time=10)
+    msg_eval = eval(repr(msg))
+    assert type(msg_eval) == MetaMessage
+    assert msg == msg_eval
+
+
+def test_frozen_unknown_meta_repr():
+    msg = FrozenUnknownMetaMessage(type_byte=99, data=[1, 2], time=10)
+    msg_eval = eval(repr(msg))
+    assert type(msg_eval) == UnknownMetaMessage
+    assert msg == msg_eval

pip install -e .[dev]
parse_log_pytest
pytest
pytest check-manifest flake8 sphinx
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
babel==2.17.0
build==1.2.2.post1
cachetools==5.5.2
certifi==2025.1.31
chardet==5.2.0
charset-normalizer==3.4.1
check-manifest==0.50
colorama==0.4.6
distlib==0.3.9
docutils==0.21.2
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
filelock==3.18.0
flake8==7.2.0
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.1.6
MarkupSafe==3.0.2
mccabe==0.7.0
-e git+https://github.com/mido/mido.git@e87384d7e5d62de361a65ab6b1d5d62750475e84#egg=mido
packaging @ file:///croot/packaging_1734472117206/work
platformdirs==4.3.7
pluggy @ file:///croot/pluggy_1733169602837/work
pycodestyle==2.13.0
pyflakes==3.3.1
Pygments==2.19.1
pyproject-api==1.9.0
pyproject_hooks==1.2.0
pytest @ file:///croot/pytest_1738938843180/work
requests==2.32.3
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tomli==2.2.1
tox==4.25.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
zipp==3.21.0

name: mido
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - babel==2.17.0
      - build==1.2.2.post1
      - cachetools==5.5.2
      - certifi==2025.1.31
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - check-manifest==0.50
      - colorama==0.4.6
      - distlib==0.3.9
      - docutils==0.21.2
      - filelock==3.18.0
      - flake8==7.2.0
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - platformdirs==4.3.7
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pyproject-api==1.9.0
      - pyproject-hooks==1.2.0
      - requests==2.32.3
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tomli==2.2.1
      - tox==4.25.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - zipp==3.21.0
prefix: /opt/conda/envs/mido

tests/messages/test_messages.py::test_repr tests/midifiles/test_meta.py::test_meta_message_repr tests/midifiles/test_meta.py::test_unknown_meta_message_repr tests/midifiles/test_midifiles.py::test_midifile_repr tests/midifiles/test_tracks.py::test_track_repr tests/test_frozen.py::test_frozen_repr tests/test_frozen.py::test_frozen_meta_repr tests/test_frozen.py::test_frozen_unknown_meta_repr
tests/messages/test_messages.py::test_msg_time_equality tests/messages/test_messages.py::test_set_type tests/messages/test_messages.py::test_encode_pitchwheel tests/messages/test_messages.py::test_decode_pitchwheel tests/messages/test_messages.py::test_encode_songpos tests/messages/test_messages.py::test_decode_songpos tests/messages/test_messages.py::test_sysex_data_is_sysexdata_object tests/messages/test_messages.py::test_sysex_data_accepts_different_types tests/messages/test_messages.py::test_copy tests/messages/test_messages.py::test_init_invalid_argument tests/messages/test_messages.py::test_copy_invalid_argument tests/messages/test_messages.py::test_copy_cant_change_type tests/messages/test_messages.py::test_copy_can_have_same_type tests/messages/test_messages.py::test_copy_handles_data_generator tests/messages/test_messages.py::test_compare_with_nonmessage tests/messages/test_messages.py::test_from_dict_default_values tests/messages/test_messages.py::test_dict_sysex_data tests/messages/test_messages.py::test_from_hex_sysex_data_type tests/midifiles/test_meta.py::test_copy_invalid_argument tests/midifiles/test_meta.py::test_copy_cant_override_type tests/midifiles/test_meta.py::TestKeySignature::test_bad_key_sig_throws_key_signature_error[bad_key_sig0] tests/midifiles/test_meta.py::TestKeySignature::test_bad_key_sig_throws_key_signature_error[bad_key_sig1] tests/midifiles/test_meta.py::TestKeySignature::test_bad_key_sig_throws_key_signature_error[bad_key_sig2] tests/midifiles/test_meta.py::TestKeySignature::test_bad_key_sig_throws_key_signature_error[bad_key_sig3] tests/midifiles/test_meta.py::TestKeySignature::test_bad_key_sig_throws_key_signature_error[bad_key_sig4] tests/midifiles/test_meta.py::TestKeySignature::test_key_signature[input_bytes0-C] tests/midifiles/test_meta.py::TestKeySignature::test_key_signature[input_bytes1-Am] tests/midifiles/test_meta.py::TestKeySignature::test_key_signature[input_bytes2-Cb] tests/midifiles/test_meta.py::TestKeySignature::test_key_signature[input_bytes3-Abm] tests/midifiles/test_meta.py::TestKeySignature::test_key_signature[input_bytes4-A#m] tests/midifiles/test_midifiles.py::test_no_tracks tests/midifiles/test_midifiles.py::test_single_message tests/midifiles/test_midifiles.py::test_too_long_message tests/midifiles/test_midifiles.py::test_two_tracks tests/midifiles/test_midifiles.py::test_empty_file tests/midifiles/test_midifiles.py::test_eof_in_track tests/midifiles/test_midifiles.py::test_invalid_data_byte_no_clipping tests/midifiles/test_midifiles.py::test_invalid_data_byte_with_clipping_high tests/midifiles/test_midifiles.py::test_meta_messages tests/midifiles/test_midifiles.py::test_meta_message_bad_key_sig_throws_key_signature_error_sharps tests/midifiles/test_midifiles.py::test_meta_message_bad_key_sig_throws_key_signature_error_flats tests/midifiles/test_midifiles.py::test_meta_messages_with_length_0 tests/midifiles/test_tracks.py::test_track_slice tests/midifiles/test_tracks.py::test_track_name tests/test_frozen.py::test_hashability tests/test_frozen.py::test_freeze_and_thaw tests/test_frozen.py::test_thawed_message_is_copy tests/test_frozen.py::test_is_frozen
mido/midifiles/midifiles.py mido/midifiles/meta.py mido/frozen.py mido/midifiles/tracks.py mido/messages/messages.py
mido/midifiles/midifiles.py mido/midifiles/meta.py mido/frozen.py mido/midifiles/tracks.py mido/messages/messages.py